diff options
author | Robin Krahl <me@robin-krahl.de> | 2018-12-11 23:50:45 +0100 |
---|---|---|
committer | Daniel Mueller <deso@posteo.net> | 2018-12-17 07:52:13 -0800 |
commit | 986ad2f782cf944990e4eda8bf88ea1821233302 (patch) | |
tree | 1717075a4eb11861c32e5c45d01e47360fb1264d /nitrokey | |
parent | e97c287c01cf22a1b582a7da9b309b58f3935d0e (diff) | |
download | nitrocli-986ad2f782cf944990e4eda8bf88ea1821233302.tar.gz nitrocli-986ad2f782cf944990e4eda8bf88ea1821233302.tar.bz2 |
Add nitrokey as a dependency to nitrocli
The nitrokey crate provides a simple interface to the Nitrokey Storage
and the Nitrokey Pro based on the libnitrokey library developed by
Nitrokey UG. The low-level bindings to this library are available in
the nitrokey-sys crate.
This patch adds version v0.2.1 of the nitrokey crate as a dependency
for nitrocli. It includes the indirect dependencies nitrokey-sys
(version 3.4.1) and rand (version 0.4.3).
Import subrepo nitrokey/:nitrokey at 2eccc96ceec2282b868891befe9cda7f941fbe7b
Import subrepo nitrokey-sys/:nitrokey-sys at f1a11ebf72610fb9cf80ac7f9f147b4ba1a5336f
Import subrepo rand/:rand at d7d5da49daf7ceb3e5940072940d495cced3a1b3
Diffstat (limited to 'nitrokey')
-rw-r--r-- | nitrokey/.gitignore | 6 | ||||
-rw-r--r-- | nitrokey/CHANGELOG.md | 22 | ||||
-rw-r--r-- | nitrokey/Cargo.toml | 22 | ||||
-rw-r--r-- | nitrokey/LICENSE | 21 | ||||
-rw-r--r-- | nitrokey/README.md | 84 | ||||
-rw-r--r-- | nitrokey/TODO.md | 46 | ||||
-rw-r--r-- | nitrokey/src/auth.rs | 412 | ||||
-rw-r--r-- | nitrokey/src/config.rs | 99 | ||||
-rw-r--r-- | nitrokey/src/device.rs | 749 | ||||
-rw-r--r-- | nitrokey/src/lib.rs | 127 | ||||
-rw-r--r-- | nitrokey/src/otp.rs | 407 | ||||
-rw-r--r-- | nitrokey/src/pws.rs | 351 | ||||
-rw-r--r-- | nitrokey/src/tests/device.rs | 304 | ||||
-rw-r--r-- | nitrokey/src/tests/mod.rs | 4 | ||||
-rw-r--r-- | nitrokey/src/tests/otp.rs | 283 | ||||
-rw-r--r-- | nitrokey/src/tests/pws.rs | 143 | ||||
-rw-r--r-- | nitrokey/src/tests/util.rs | 11 | ||||
-rw-r--r-- | nitrokey/src/util.rs | 171 |
18 files changed, 3262 insertions, 0 deletions
diff --git a/nitrokey/.gitignore b/nitrokey/.gitignore new file mode 100644 index 0000000..4cdf3b3 --- /dev/null +++ b/nitrokey/.gitignore @@ -0,0 +1,6 @@ + +/target +/nitrokey-sys/target +**/*.rs.bk +Cargo.lock +*.swp diff --git a/nitrokey/CHANGELOG.md b/nitrokey/CHANGELOG.md new file mode 100644 index 0000000..a60d6a7 --- /dev/null +++ b/nitrokey/CHANGELOG.md @@ -0,0 +1,22 @@ +# v0.2.1 (2018-12-10) + +- Re-export `device::{StorageStatus, VolumeStatus}` in `lib.rs`. + +# v0.2.0 (2018-12-10) + +- Update to libnitrokey v3.4.1. +- Major refactoring of the existing code structure. +- Add support for most of the Nitrokey Pro features and some of the Nitrokey + Storage features. See the `TODO.md` file for more details about the missing + functionality. + +# v0.1.1 (2018-05-21) + +- Update the `nitrokey-sys` dependency to version 3.3.0. Now `libnitrokey` + is built from source and `bindgen` is no longer a build dependency. +- Add `get_minor_firmware_version` to `Device`. +- Use `NK_login_enum` instead of `NK_login` in `Device::connect`. + +# v0.1.0 (2018-05-19) + +- Initial release diff --git a/nitrokey/Cargo.toml b/nitrokey/Cargo.toml new file mode 100644 index 0000000..dad751b --- /dev/null +++ b/nitrokey/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "nitrokey" +version = "0.2.1" +authors = ["Robin Krahl <robin.krahl@ireas.org>"] +homepage = "https://code.ireas.org/nitrokey-rs/" +repository = "https://git.ireas.org/nitrokey-rs/" +documentation = "https://docs.rs/nitrokey" +description = "Bindings to libnitrokey for communication with Nitrokey devices" +keywords = ["nitrokey", "otp"] +categories = ["api-bindings"] +readme = "README.md" +license = "MIT" + +[features] +test-no-device = [] +test-pro = [] +test-storage = [] + +[dependencies] +libc = "0.2" +nitrokey-sys = "3.4.1" +rand = "0.4" diff --git a/nitrokey/LICENSE b/nitrokey/LICENSE new file mode 100644 index 0000000..1a3601d --- /dev/null +++ b/nitrokey/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Robin Krahl <robin.krahl@ireas.org> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nitrokey/README.md b/nitrokey/README.md new file mode 100644 index 0000000..6039943 --- /dev/null +++ b/nitrokey/README.md @@ -0,0 +1,84 @@ +# nitrokey-rs + +A libnitrokey wrapper for Rust providing access to Nitrokey devices. + +[Documentation][] + +```toml +[dependencies] +nitrokey = "0.2.1" +``` + +## Compatibility + +The required [`libnitrokey`][] version is built from source. The host system +must provide `libhidapi-libusb0` in the default library search path. + +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. + +### Unsupported Functions + +The following functions provided by `libnitrokey` are deliberately not +supported by `nitrokey-rs`: + +- `NK_get_time()`. This method is useless as it will always cause a timestamp + error on the device (see [pull request #114][] for `libnitrokey` for details). +- `NK_get_status()`. This method only provides a string representation of + data that can be accessed by other methods (firmware version, serial number, + configuration). +- `NK_get_status_storage_as_string()`. This method only provides an incomplete + string representation of the data returned by `NK_get_status_storage`. + +## Tests + +This crate has three test suites that can be selected using features. One test +suite (feature `test-no-device`) assumes that no Nitrokey device is connected. +The two other test suites require a Nitrokey Pro (feature `test-pro`) or a +Nitrokey Storage (feature `test-storage`) to be connected. + +Use the `--features` option for Cargo to select one of the test suites. You +cannot select more than one of the test suites at the same time. Note that the +test suites that require a Nitrokey device assume that the device’s passwords +are the factory defaults (admin password `12345678` and user password +`123456`). Running the test suite with a device with different passwords might +lock your device! Also note that the test suite might delete or overwrite data +on all connected devices. + +As the tests currently are not synchronized, you have to make sure that they +are not executed in parallel. To do so, pass the option `--test-threads 1` to +the test executable. + +In conclusion, you can use these commands to run the test suites: + +``` +$ cargo test --features test-no-device -- --test-threads 1 +$ cargo test --features test-pro -- --test-threads 1 +$ cargo test --features test-storage -- --test-threads 1 +``` + +The `totp_no_pin` and `totp_pin` tests can occasionally fail due to bad timing. + +## Acknowledgments + +Thanks to Nitrokey UG for providing a Nitrokey Storage to support the +development of this crate. + +## Contact + +For bug reports, patches, feature requests or other messages, please send a +mail to [nitrokey-rs-dev@ireas.org][]. + +## License + +This project is licensed under the [MIT License][]. `libnitrokey` is licensed +under the [LGPL-3.0][]. + +[Documentation]: https://docs.rs/nitrokey +[`libnitrokey`]: https://github.com/nitrokey/libnitrokey +[nitrokey-rs-dev@ireas.org]: mailto:nitrokey-rs-dev@ireas.org +[pull request #114]: https://github.com/Nitrokey/libnitrokey/pull/114 +[MIT license]: https://opensource.org/licenses/MIT +[LGPL-3.0]: https://opensource.org/licenses/lgpl-3.0.html diff --git a/nitrokey/TODO.md b/nitrokey/TODO.md new file mode 100644 index 0000000..6086ad8 --- /dev/null +++ b/nitrokey/TODO.md @@ -0,0 +1,46 @@ +- Add support for the currently unsupported commands: + - `NK_set_unencrypted_volume_rorw_pin_type_user` + - `NK_factory_reset` + - `NK_build_aes_key` + - `NK_is_AES_supported` + - `NK_send_startup` + - `NK_unlock_hidden_volume` + - `NK_lock_hidden_volume` + - `NK_create_hidden_volume` + - `NK_set_unencrypted_read_only` + - `NK_set_unencrypted_read_only_admin` + - `NK_set_unencrypted_read_write` + - `NK_set_unencrypted_read_write_admin` + - `NK_set_encrypted_read_only` + - `NK_set_encrypted_read_write` + - `NK_enable_firmware_update` + - `NK_export_firmware` + - `NK_clear_new_sd_card_warning` + - `NK_fill_SD_card_with_random_data` + - `NK_change_update_password` + - `NK_get_SD_usage_data_as_string` + - `NK_get_progress_bar_value` + - `NK_list_devices_by_cpuID` + - `NK_connect_with_ID` + - `NK_get_device_model` + - `NK_get_library_version` + - `NK_get_major_library_version` + - `NK_get_minor_libray_version` + - `NK_get_storage_production_info` + - `NK_totp_set_time_soft` + - `NK_wink` +- Fix timing issues with the `totp_no_pin` and `totp_pin` test cases. +- Clear passwords from memory. +- Find a nicer syntax for the `write_config` test. +- Prevent construction of internal types. +- More specific error checking in the tests. +- Differentiate empty strings and errors (see `result_from_string`). +- Check integer conversions. +- Consider implementing `Into<CommandError>` for `(Device, CommandError)` +- 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/src/auth.rs b/nitrokey/src/auth.rs new file mode 100644 index 0000000..0918222 --- /dev/null +++ b/nitrokey/src/auth.rs @@ -0,0 +1,412 @@ +use config::{Config, RawConfig}; +use device::{Device, DeviceWrapper, Pro, Storage}; +use nitrokey_sys; +use otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData, RawOtpSlotData}; +use std::ops::Deref; +use std::os::raw::c_int; +use util::{generate_password, get_command_result, get_cstring, result_from_string, CommandError}; + +static TEMPORARY_PASSWORD_LENGTH: usize = 25; + +/// Provides methods to authenticate as a user or as an admin using a PIN. The authenticated +/// methods will consume the current device instance. On success, they return the authenticated +/// device. Otherwise, they return the current unauthenticated device and the error code. +pub trait Authenticate { + /// Performs user authentication. This method consumes the device. If successful, an + /// authenticated device is returned. Otherwise, the current unauthenticated device and the + /// error are returned. + /// + /// This method generates a random temporary password that is used for all operations that + /// require user access. + /// + /// # Errors + /// + /// - [`InvalidString`][] if the provided user password contains a null byte + /// - [`RngError`][] if the generation of the temporary password failed + /// - [`WrongPassword`][] if the provided user password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{Authenticate, DeviceWrapper, User}; + /// # use nitrokey::CommandError; + /// + /// fn perform_user_task(device: &User<DeviceWrapper>) {} + /// fn perform_other_task(device: &DeviceWrapper) {} + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let device = match device.authenticate_user("123456") { + /// Ok(user) => { + /// perform_user_task(&user); + /// user.device() + /// }, + /// Err((device, err)) => { + /// println!("Could not authenticate as user: {}", err); + /// device + /// }, + /// }; + /// perform_other_task(&device); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`RngError`]: enum.CommandError.html#variant.RngError + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + fn authenticate_user(self, password: &str) -> Result<User<Self>, (Self, CommandError)> + where + Self: Device + Sized; + + /// Performs admin authentication. This method consumes the device. If successful, an + /// authenticated device is returned. Otherwise, the current unauthenticated device and the + /// error are returned. + /// + /// This method generates a random temporary password that is used for all operations that + /// require admin access. + /// + /// # Errors + /// + /// - [`InvalidString`][] if the provided admin password contains a null byte + /// - [`RngError`][] if the generation of the temporary password failed + /// - [`WrongPassword`][] if the provided admin password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{Authenticate, Admin, DeviceWrapper}; + /// # use nitrokey::CommandError; + /// + /// fn perform_admin_task(device: &Admin<DeviceWrapper>) {} + /// fn perform_other_task(device: &DeviceWrapper) {} + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let device = match device.authenticate_admin("123456") { + /// Ok(admin) => { + /// perform_admin_task(&admin); + /// admin.device() + /// }, + /// Err((device, err)) => { + /// println!("Could not authenticate as admin: {}", err); + /// device + /// }, + /// }; + /// perform_other_task(&device); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`RngError`]: enum.CommandError.html#variant.RngError + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + fn authenticate_admin(self, password: &str) -> Result<Admin<Self>, (Self, CommandError)> + where + Self: Device + Sized; +} + +trait AuthenticatedDevice<T> { + fn new(device: T, temp_password: Vec<u8>) -> Self; +} + +/// A Nitrokey device with user authentication. +/// +/// To obtain an instance of this struct, use the [`authenticate_user`][] method from the +/// [`Authenticate`][] trait. To get back to an unauthenticated device, use the [`device`][] +/// method. +/// +/// [`Authenticate`]: trait.Authenticate.html +/// [`authenticate_admin`]: trait.Authenticate.html#method.authenticate_admin +/// [`device`]: #method.device +#[derive(Debug)] +pub struct User<T: Device> { + device: T, + temp_password: Vec<u8>, +} + +/// A Nitrokey device with admin authentication. +/// +/// To obtain an instance of this struct, use the [`authenticate_admin`][] method from the +/// [`Authenticate`][] trait. To get back to an unauthenticated device, use the [`device`][] +/// method. +/// +/// [`Authenticate`]: trait.Authenticate.html +/// [`authenticate_admin`]: trait.Authenticate.html#method.authenticate_admin +/// [`device`]: #method.device +#[derive(Debug)] +pub struct Admin<T: Device> { + device: T, + temp_password: Vec<u8>, +} + +fn authenticate<D, A, T>(device: D, password: &str, callback: T) -> Result<A, (D, CommandError)> +where + D: Device, + A: AuthenticatedDevice<D>, + T: Fn(*const i8, *const i8) -> c_int, +{ + let temp_password = match generate_password(TEMPORARY_PASSWORD_LENGTH) { + Ok(pw) => pw, + Err(_) => return Err((device, CommandError::RngError)), + }; + let password = match get_cstring(password) { + Ok(password) => password, + Err(err) => return Err((device, err)), + }; + let password_ptr = password.as_ptr(); + let temp_password_ptr = temp_password.as_ptr() as *const i8; + return match callback(password_ptr, temp_password_ptr) { + 0 => Ok(A::new(device, temp_password)), + rv => Err((device, CommandError::from(rv))), + }; +} + +fn authenticate_user_wrapper<T, C>( + device: T, + constructor: C, + password: &str, +) -> Result<User<DeviceWrapper>, (DeviceWrapper, CommandError)> +where + T: Device, + C: Fn(T) -> DeviceWrapper, +{ + let result = device.authenticate_user(password); + match result { + Ok(user) => Ok(User::new(constructor(user.device), user.temp_password)), + Err((device, err)) => Err((constructor(device), err)), + } +} + +fn authenticate_admin_wrapper<T, C>( + device: T, + constructor: C, + password: &str, +) -> Result<Admin<DeviceWrapper>, (DeviceWrapper, CommandError)> +where + T: Device, + C: Fn(T) -> DeviceWrapper, +{ + let result = device.authenticate_admin(password); + match result { + Ok(user) => Ok(Admin::new(constructor(user.device), user.temp_password)), + Err((device, err)) => Err((constructor(device), err)), + } +} + +impl<T: Device> User<T> { + /// Forgets the user authentication and returns an unauthenticated device. This method + /// consumes the authenticated device. It does not perform any actual commands on the + /// Nitrokey. + pub fn device(self) -> T { + self.device + } +} + +impl<T: Device> Deref for User<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.device + } +} + +impl<T: Device> GenerateOtp for User<T> { + fn get_hotp_code(&self, slot: u8) -> Result<String, CommandError> { + unsafe { + let temp_password_ptr = self.temp_password.as_ptr() as *const i8; + return result_from_string(nitrokey_sys::NK_get_hotp_code_PIN(slot, temp_password_ptr)); + } + } + + fn get_totp_code(&self, slot: u8) -> Result<String, CommandError> { + unsafe { + let temp_password_ptr = self.temp_password.as_ptr() as *const i8; + return result_from_string(nitrokey_sys::NK_get_totp_code_PIN( + slot, + 0, + 0, + 0, + temp_password_ptr, + )); + } + } +} + +impl<T: Device> AuthenticatedDevice<T> for User<T> { + fn new(device: T, temp_password: Vec<u8>) -> Self { + User { + device, + temp_password, + } + } +} + +impl<T: Device> Deref for Admin<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.device + } +} + +impl<T: Device> Admin<T> { + /// Forgets the user authentication and returns an unauthenticated device. This method + /// consumes the authenticated device. It does not perform any actual commands on the + /// Nitrokey. + pub fn device(self) -> T { + self.device + } + + /// Writes the given configuration to the Nitrokey device. + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if the provided numlock, capslock or scrolllock slot is larger than two + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{Authenticate, Config}; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let config = Config::new(None, None, None, false); + /// match device.authenticate_admin("12345678") { + /// Ok(admin) => { + /// admin.write_config(config); + /// () + /// }, + /// Err((_, err)) => println!("Could not authenticate as admin: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + pub fn write_config(&self, config: Config) -> Result<(), CommandError> { + let raw_config = RawConfig::try_from(config)?; + unsafe { + get_command_result(nitrokey_sys::NK_write_config( + raw_config.numlock, + raw_config.capslock, + raw_config.scrollock, + raw_config.user_password, + false, + self.temp_password.as_ptr() as *const i8, + )) + } + } + + fn write_otp_slot<C>(&self, data: OtpSlotData, callback: C) -> Result<(), CommandError> + where + C: Fn(RawOtpSlotData, *const i8) -> c_int, + { + let raw_data = RawOtpSlotData::new(data)?; + let temp_password_ptr = self.temp_password.as_ptr() as *const i8; + get_command_result(callback(raw_data, temp_password_ptr)) + } +} + +impl<T: Device> ConfigureOtp for Admin<T> { + fn write_hotp_slot(&self, data: OtpSlotData, counter: u64) -> Result<(), CommandError> { + self.write_otp_slot(data, |raw_data: RawOtpSlotData, temp_password_ptr| unsafe { + nitrokey_sys::NK_write_hotp_slot( + raw_data.number, + raw_data.name.as_ptr(), + raw_data.secret.as_ptr(), + counter, + raw_data.mode == OtpMode::EightDigits, + raw_data.use_enter, + raw_data.use_token_id, + raw_data.token_id.as_ptr(), + temp_password_ptr, + ) + }) + } + + fn write_totp_slot(&self, data: OtpSlotData, time_window: u16) -> Result<(), CommandError> { + self.write_otp_slot(data, |raw_data: RawOtpSlotData, temp_password_ptr| unsafe { + nitrokey_sys::NK_write_totp_slot( + raw_data.number, + raw_data.name.as_ptr(), + raw_data.secret.as_ptr(), + time_window, + raw_data.mode == OtpMode::EightDigits, + raw_data.use_enter, + raw_data.use_token_id, + raw_data.token_id.as_ptr(), + temp_password_ptr, + ) + }) + } + + fn erase_hotp_slot(&self, slot: u8) -> Result<(), CommandError> { + let temp_password_ptr = self.temp_password.as_ptr() as *const i8; + unsafe { get_command_result(nitrokey_sys::NK_erase_hotp_slot(slot, temp_password_ptr)) } + } + + fn erase_totp_slot(&self, slot: u8) -> Result<(), CommandError> { + let temp_password_ptr = self.temp_password.as_ptr() as *const i8; + unsafe { get_command_result(nitrokey_sys::NK_erase_totp_slot(slot, temp_password_ptr)) } + } +} + +impl<T: Device> AuthenticatedDevice<T> for Admin<T> { + fn new(device: T, temp_password: Vec<u8>) -> Self { + Admin { + device, + temp_password, + } + } +} + +impl Authenticate for DeviceWrapper { + fn authenticate_user(self, password: &str) -> Result<User<Self>, (Self, CommandError)> { + match self { + DeviceWrapper::Storage(storage) => { + authenticate_user_wrapper(storage, DeviceWrapper::Storage, password) + } + DeviceWrapper::Pro(pro) => authenticate_user_wrapper(pro, DeviceWrapper::Pro, password), + } + } + + fn authenticate_admin(self, password: &str) -> Result<Admin<Self>, (Self, CommandError)> { + match self { + DeviceWrapper::Storage(storage) => { + authenticate_admin_wrapper(storage, DeviceWrapper::Storage, password) + } + DeviceWrapper::Pro(pro) => { + authenticate_admin_wrapper(pro, DeviceWrapper::Pro, password) + } + } + } +} + +impl Authenticate for Pro { + fn authenticate_user(self, password: &str) -> Result<User<Self>, (Self, CommandError)> { + authenticate(self, password, |password_ptr, temp_password_ptr| unsafe { + nitrokey_sys::NK_user_authenticate(password_ptr, temp_password_ptr) + }) + } + + fn authenticate_admin(self, password: &str) -> Result<Admin<Self>, (Self, CommandError)> { + authenticate(self, password, |password_ptr, temp_password_ptr| unsafe { + nitrokey_sys::NK_first_authenticate(password_ptr, temp_password_ptr) + }) + } +} + +impl Authenticate for Storage { + fn authenticate_user(self, password: &str) -> Result<User<Self>, (Self, CommandError)> { + authenticate(self, password, |password_ptr, temp_password_ptr| unsafe { + nitrokey_sys::NK_user_authenticate(password_ptr, temp_password_ptr) + }) + } + + fn authenticate_admin(self, password: &str) -> Result<Admin<Self>, (Self, CommandError)> { + authenticate(self, password, |password_ptr, temp_password_ptr| unsafe { + nitrokey_sys::NK_first_authenticate(password_ptr, temp_password_ptr) + }) + } +} diff --git a/nitrokey/src/config.rs b/nitrokey/src/config.rs new file mode 100644 index 0000000..33bf256 --- /dev/null +++ b/nitrokey/src/config.rs @@ -0,0 +1,99 @@ +use util::CommandError; + +/// The configuration for a Nitrokey. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Config { + /// If set, the stick will generate a code from the HOTP slot with the given number if numlock + /// is pressed. The slot number must be 0, 1 or 2. + pub numlock: Option<u8>, + /// If set, the stick will generate a code from the HOTP slot with the given number if capslock + /// is pressed. The slot number must be 0, 1 or 2. + pub capslock: Option<u8>, + /// If set, the stick will generate a code from the HOTP slot with the given number if + /// scrollock is pressed. The slot number must be 0, 1 or 2. + pub scrollock: Option<u8>, + /// If set, OTP generation using [`get_hotp_code`][] or [`get_totp_code`][] requires user + /// authentication. Otherwise, OTPs can be generated without authentication. + /// + /// [`get_hotp_code`]: trait.ProvideOtp.html#method.get_hotp_code + /// [`get_totp_code`]: trait.ProvideOtp.html#method.get_totp_code + pub user_password: bool, +} + +#[derive(Debug)] +pub struct RawConfig { + pub numlock: u8, + pub capslock: u8, + pub scrollock: u8, + pub user_password: bool, +} + +fn config_otp_slot_to_option(value: u8) -> Option<u8> { + if value < 3 { + return Some(value); + } + None +} + +fn option_to_config_otp_slot(value: Option<u8>) -> Result<u8, CommandError> { + match value { + Some(value) => { + if value < 3 { + Ok(value) + } else { + Err(CommandError::InvalidSlot) + } + } + None => Ok(255), + } +} + +impl Config { + /// Constructs a new instance of this struct. + pub fn new( + numlock: Option<u8>, + capslock: Option<u8>, + scrollock: Option<u8>, + user_password: bool, + ) -> Config { + Config { + numlock, + capslock, + scrollock, + user_password, + } + } +} + +impl RawConfig { + pub fn try_from(config: Config) -> Result<RawConfig, CommandError> { + Ok(RawConfig { + numlock: option_to_config_otp_slot(config.numlock)?, + capslock: option_to_config_otp_slot(config.capslock)?, + scrollock: option_to_config_otp_slot(config.scrollock)?, + user_password: config.user_password, + }) + } +} + +impl From<[u8; 5]> for RawConfig { + fn from(data: [u8; 5]) -> Self { + RawConfig { + numlock: data[0], + capslock: data[1], + scrollock: data[2], + user_password: data[3] != 0, + } + } +} + +impl Into<Config> for RawConfig { + fn into(self) -> Config { + Config { + numlock: config_otp_slot_to_option(self.numlock), + capslock: config_otp_slot_to_option(self.capslock), + scrollock: config_otp_slot_to_option(self.scrollock), + user_password: self.user_password, + } + } +} diff --git a/nitrokey/src/device.rs b/nitrokey/src/device.rs new file mode 100644 index 0000000..f135261 --- /dev/null +++ b/nitrokey/src/device.rs @@ -0,0 +1,749 @@ +use auth::Authenticate; +use config::{Config, RawConfig}; +use libc; +use nitrokey_sys; +use otp::GenerateOtp; +use pws::GetPasswordSafe; +use util::{get_command_result, get_cstring, get_last_error, result_from_string, CommandError}; + +/// Available Nitrokey models. +#[derive(Debug, PartialEq)] +enum Model { + /// The Nitrokey Storage. + Storage, + /// The Nitrokey Pro. + Pro, +} + +/// A wrapper for a Nitrokey device of unknown type. +/// +/// Use the function [`connect`][] to obtain a wrapped instance. The wrapper implements all traits +/// that are shared between all Nitrokey devices so that the shared functionality can be used +/// without knowing the type of the underlying device. If you want to use functionality that is +/// not available for all devices, you have to extract the device. +/// +/// # Examples +/// +/// Authentication with error handling: +/// +/// ```no_run +/// use nitrokey::{Authenticate, DeviceWrapper, User}; +/// # use nitrokey::CommandError; +/// +/// fn perform_user_task(device: &User<DeviceWrapper>) {} +/// fn perform_other_task(device: &DeviceWrapper) {} +/// +/// # fn try_main() -> Result<(), CommandError> { +/// let device = nitrokey::connect()?; +/// let device = match device.authenticate_user("123456") { +/// Ok(user) => { +/// perform_user_task(&user); +/// user.device() +/// }, +/// Err((device, err)) => { +/// println!("Could not authenticate as user: {}", err); +/// device +/// }, +/// }; +/// perform_other_task(&device); +/// # Ok(()) +/// # } +/// ``` +/// +/// Device-specific commands: +/// +/// ```no_run +/// use nitrokey::{DeviceWrapper, Storage}; +/// # use nitrokey::CommandError; +/// +/// fn perform_common_task(device: &DeviceWrapper) {} +/// fn perform_storage_task(device: &Storage) {} +/// +/// # fn try_main() -> Result<(), CommandError> { +/// let device = nitrokey::connect()?; +/// perform_common_task(&device); +/// match device { +/// DeviceWrapper::Storage(storage) => perform_storage_task(&storage), +/// _ => (), +/// }; +/// # Ok(()) +/// # } +/// ``` +/// +/// [`connect`]: fn.connect.html +// TODO: add example for Storage-specific code +#[derive(Debug)] +pub enum DeviceWrapper { + /// A Nitrokey Storage device. + Storage(Storage), + /// A Nitrokey Pro device. + Pro(Pro), +} + +/// A Nitrokey Pro device without user or admin authentication. +/// +/// Use the global function [`connect`][] to obtain an instance wrapper or the method +/// [`connect`][`Pro::connect`] to directly obtain an instance. If you want to execute a command +/// that requires user or admin authentication, use [`authenticate_admin`][] or +/// [`authenticate_user`][]. +/// +/// # Examples +/// +/// Authentication with error handling: +/// +/// ```no_run +/// use nitrokey::{Authenticate, User, Pro}; +/// # use nitrokey::CommandError; +/// +/// fn perform_user_task(device: &User<Pro>) {} +/// fn perform_other_task(device: &Pro) {} +/// +/// # fn try_main() -> Result<(), CommandError> { +/// let device = nitrokey::Pro::connect()?; +/// let device = match device.authenticate_user("123456") { +/// Ok(user) => { +/// perform_user_task(&user); +/// user.device() +/// }, +/// Err((device, err)) => { +/// println!("Could not authenticate as user: {}", err); +/// device +/// }, +/// }; +/// perform_other_task(&device); +/// # Ok(()) +/// # } +/// ``` +/// +/// [`authenticate_admin`]: trait.Authenticate.html#method.authenticate_admin +/// [`authenticate_user`]: trait.Authenticate.html#method.authenticate_user +/// [`connect`]: fn.connect.html +/// [`Pro::connect`]: #method.connect +#[derive(Debug)] +pub struct Pro {} + +/// A Nitrokey Storage device without user or admin authentication. +/// +/// Use the global function [`connect`][] to obtain an instance wrapper or the method +/// [`connect`][`Storage::connect`] to directly obtain an instance. If you want to execute a +/// command that requires user or admin authentication, use [`authenticate_admin`][] or +/// [`authenticate_user`][]. +/// +/// # Examples +/// +/// Authentication with error handling: +/// +/// ```no_run +/// use nitrokey::{Authenticate, User, Storage}; +/// # use nitrokey::CommandError; +/// +/// fn perform_user_task(device: &User<Storage>) {} +/// fn perform_other_task(device: &Storage) {} +/// +/// # fn try_main() -> Result<(), CommandError> { +/// let device = nitrokey::Storage::connect()?; +/// let device = match device.authenticate_user("123456") { +/// Ok(user) => { +/// perform_user_task(&user); +/// user.device() +/// }, +/// Err((device, err)) => { +/// println!("Could not authenticate as user: {}", err); +/// device +/// }, +/// }; +/// perform_other_task(&device); +/// # Ok(()) +/// # } +/// ``` +/// +/// [`authenticate_admin`]: trait.Authenticate.html#method.authenticate_admin +/// [`authenticate_user`]: trait.Authenticate.html#method.authenticate_user +/// [`connect`]: fn.connect.html +/// [`Storage::connect`]: #method.connect +#[derive(Debug)] +pub struct Storage {} + +/// The status of a volume on a Nitrokey Storage device. +#[derive(Debug)] +pub struct VolumeStatus { + /// Indicates whether the volume is read-only. + pub read_only: bool, + /// Indicates whether the volume is active. + pub active: bool, +} + +/// The status of a Nitrokey Storage device. +#[derive(Debug)] +pub struct StorageStatus { + /// The status of the unencrypted volume. + pub unencrypted_volume: VolumeStatus, + /// The status of the encrypted volume. + pub encrypted_volume: VolumeStatus, + /// The status of the hidden volume. + pub hidden_volume: VolumeStatus, + /// The major firmware version, e. g. 0 in v0.40. + pub firmware_version_major: u8, + /// The minor firmware version, e. g. 40 in v0.40. + pub firmware_version_minor: u8, + /// Indicates whether the firmware is locked. + pub firmware_locked: bool, + /// The serial number of the SD card in the Storage stick. + pub serial_number_sd_card: u32, + /// The serial number of the smart card in the Storage stick. + pub serial_number_smart_card: u32, + /// The number of remaining login attempts for the user PIN. + pub user_retry_count: u8, + /// The number of remaining login attempts for the admin PIN. + pub admin_retry_count: u8, + /// Indicates whether a new SD card was found. + pub new_sd_card_found: bool, + /// Indicates whether the SD card is filled with random characters. + pub filled_with_random: bool, + /// Indicates whether the stick has been initialized by generating + /// the AES keys. + pub stick_initialized: bool, +} + +/// A Nitrokey device. +/// +/// This trait provides the commands that can be executed without authentication and that are +/// present on all supported Nitrokey devices. +pub trait Device: Authenticate + GetPasswordSafe + GenerateOtp { + /// Returns the serial number of the Nitrokey device. The serial number is the string + /// representation of a hex number. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.get_serial_number() { + /// Ok(number) => println!("serial no: {}", number), + /// Err(err) => println!("Could not get serial number: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + fn get_serial_number(&self) -> Result<String, CommandError> { + unsafe { result_from_string(nitrokey_sys::NK_device_serial_number()) } + } + + /// Returns the number of remaining authentication attempts for the user. The total number of + /// available attempts is three. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let count = device.get_user_retry_count(); + /// println!("{} remaining authentication attempts (user)", count); + /// # Ok(()) + /// # } + /// ``` + fn get_user_retry_count(&self) -> u8 { + unsafe { nitrokey_sys::NK_get_user_retry_count() } + } + + /// Returns the number of remaining authentication attempts for the admin. The total number of + /// available attempts is three. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let count = device.get_admin_retry_count(); + /// println!("{} remaining authentication attempts (admin)", count); + /// # Ok(()) + /// # } + /// ``` + fn get_admin_retry_count(&self) -> u8 { + unsafe { nitrokey_sys::NK_get_admin_retry_count() } + } + + /// Returns the major part of the firmware version (should be zero). + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// println!( + /// "Firmware version: {}.{}", + /// device.get_major_firmware_version(), + /// device.get_minor_firmware_version(), + /// ); + /// # Ok(()) + /// # } + /// ``` + fn get_major_firmware_version(&self) -> i32 { + unsafe { nitrokey_sys::NK_get_major_firmware_version() } + } + + /// Returns the minor part of the firmware version (for example 8 for version 0.8). + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// println!( + /// "Firmware version: {}.{}", + /// device.get_major_firmware_version(), + /// device.get_minor_firmware_version(), + /// ); + /// # Ok(()) + /// # } + fn get_minor_firmware_version(&self) -> i32 { + unsafe { nitrokey_sys::NK_get_minor_firmware_version() } + } + + /// Returns the current configuration of the Nitrokey device. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let config = device.get_config()?; + /// println!("numlock binding: {:?}", config.numlock); + /// println!("capslock binding: {:?}", config.capslock); + /// println!("scrollock binding: {:?}", config.scrollock); + /// println!("require password for OTP: {:?}", config.user_password); + /// # Ok(()) + /// # } + /// ``` + fn get_config(&self) -> Result<Config, CommandError> { + unsafe { + let config_ptr = nitrokey_sys::NK_read_config(); + if config_ptr.is_null() { + return Err(get_last_error()); + } + let config_array_ptr = config_ptr as *const [u8; 5]; + let raw_config = RawConfig::from(*config_array_ptr); + libc::free(config_ptr as *mut libc::c_void); + return Ok(raw_config.into()); + } + } + + /// Changes the administrator PIN. + /// + /// # Errors + /// + /// - [`InvalidString`][] if one of the provided passwords contains a null byte + /// - [`WrongPassword`][] if the current admin password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.change_admin_pin("12345678", "12345679") { + /// Ok(()) => println!("Updated admin PIN."), + /// Err(err) => println!("Failed to update admin PIN: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + fn change_admin_pin(&self, current: &str, new: &str) -> Result<(), CommandError> { + let current_string = get_cstring(current)?; + let new_string = get_cstring(new)?; + unsafe { + get_command_result(nitrokey_sys::NK_change_admin_PIN( + current_string.as_ptr(), + new_string.as_ptr(), + )) + } + } + + /// Changes the user PIN. + /// + /// # Errors + /// + /// - [`InvalidString`][] if one of the provided passwords contains a null byte + /// - [`WrongPassword`][] if the current user password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.change_user_pin("123456", "123457") { + /// Ok(()) => println!("Updated admin PIN."), + /// Err(err) => println!("Failed to update admin PIN: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + fn change_user_pin(&self, current: &str, new: &str) -> Result<(), CommandError> { + let current_string = get_cstring(current)?; + let new_string = get_cstring(new)?; + unsafe { + get_command_result(nitrokey_sys::NK_change_user_PIN( + current_string.as_ptr(), + new_string.as_ptr(), + )) + } + } + + /// Unlocks the user PIN after three failed login attempts and sets it to the given value. + /// + /// # Errors + /// + /// - [`InvalidString`][] if one of the provided passwords contains a null byte + /// - [`WrongPassword`][] if the admin password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.unlock_user_pin("12345678", "123456") { + /// Ok(()) => println!("Unlocked user PIN."), + /// Err(err) => println!("Failed to unlock user PIN: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + fn unlock_user_pin(&self, admin_pin: &str, user_pin: &str) -> Result<(), CommandError> { + let admin_pin_string = get_cstring(admin_pin)?; + let user_pin_string = get_cstring(user_pin)?; + unsafe { + get_command_result(nitrokey_sys::NK_unlock_user_password( + admin_pin_string.as_ptr(), + user_pin_string.as_ptr(), + )) + } + } + + /// Locks the Nitrokey device. + /// + /// This disables the password store if it has been unlocked. On the Nitrokey Storage, this + /// also disables the volumes if they have been enabled. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.lock() { + /// Ok(()) => println!("Locked the Nitrokey device."), + /// Err(err) => println!("Could not lock the Nitrokey device: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + fn lock(&self) -> Result<(), CommandError> { + unsafe { get_command_result(nitrokey_sys::NK_lock_device()) } + } +} + +/// Connects to a Nitrokey device. This method can be used to connect to any connected device, +/// both a Nitrokey Pro and a Nitrokey Storage. +/// +/// # Example +/// +/// ``` +/// use nitrokey::DeviceWrapper; +/// +/// fn do_something(device: DeviceWrapper) {} +/// +/// match nitrokey::connect() { +/// Ok(device) => do_something(device), +/// Err(err) => println!("Could not connect to a Nitrokey: {}", err), +/// } +/// ``` +pub fn connect() -> Result<DeviceWrapper, CommandError> { + unsafe { + match nitrokey_sys::NK_login_auto() { + 1 => match get_connected_device() { + Some(wrapper) => Ok(wrapper), + None => Err(CommandError::Unknown), + }, + _ => Err(CommandError::Unknown), + } + } +} + +fn get_connected_model() -> Option<Model> { + unsafe { + match 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 create_device_wrapper(model: Model) -> DeviceWrapper { + match model { + Model::Pro => DeviceWrapper::Pro(Pro {}), + Model::Storage => DeviceWrapper::Storage(Storage {}), + } +} + +fn get_connected_device() -> Option<DeviceWrapper> { + get_connected_model().map(create_device_wrapper) +} + +fn connect_model(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 } +} + +impl DeviceWrapper { + fn device(&self) -> &Device { + match *self { + DeviceWrapper::Storage(ref storage) => storage, + DeviceWrapper::Pro(ref pro) => pro, + } + } +} + +impl GenerateOtp for DeviceWrapper { + fn get_hotp_slot_name(&self, slot: u8) -> Result<String, CommandError> { + self.device().get_hotp_slot_name(slot) + } + + fn get_totp_slot_name(&self, slot: u8) -> Result<String, CommandError> { + self.device().get_totp_slot_name(slot) + } + + fn get_hotp_code(&self, slot: u8) -> Result<String, CommandError> { + self.device().get_hotp_code(slot) + } + + fn get_totp_code(&self, slot: u8) -> Result<String, CommandError> { + self.device().get_totp_code(slot) + } +} + +impl Device for DeviceWrapper {} + +impl Pro { + pub fn connect() -> Result<Pro, CommandError> { + // TODO: maybe Option instead of Result? + match connect_model(Model::Pro) { + true => Ok(Pro {}), + false => Err(CommandError::Unknown), + } + } +} + +impl Drop for Pro { + fn drop(&mut self) { + unsafe { + nitrokey_sys::NK_logout(); + } + } +} + +impl Device for Pro {} + +impl GenerateOtp for Pro {} + +impl Storage { + pub fn connect() -> Result<Storage, CommandError> { + // TODO: maybe Option instead of Result? + match connect_model(Model::Storage) { + true => Ok(Storage {}), + false => Err(CommandError::Unknown), + } + } + + /// Enables the encrypted storage volume. + /// + /// Once the encrypted volume is enabled, it is presented to the operating system as a block + /// device. The API does not provide any information on the name or path of this block device. + /// + /// # Errors + /// + /// - [`InvalidString`][] if the provided password contains a null byte + /// - [`WrongPassword`][] if the provided user password is wrong + /// + /// # Example + /// + /// ```no_run + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::Storage::connect()?; + /// match device.enable_encrypted_volume("123456") { + /// Ok(()) => println!("Enabled the encrypted volume."), + /// Err(err) => println!("Could not enable the encrypted volume: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + pub fn enable_encrypted_volume(&self, user_pin: &str) -> Result<(), CommandError> { + let user_pin = get_cstring(user_pin)?; + unsafe { get_command_result(nitrokey_sys::NK_unlock_encrypted_volume(user_pin.as_ptr())) } + } + + /// Disables the encrypted storage volume. + /// + /// Once the volume is disabled, it can be no longer accessed as a block device. If the + /// encrypted volume has not been enabled, this method still returns a success. + /// + /// # Example + /// + /// ```no_run + /// # use nitrokey::CommandError; + /// + /// fn use_volume() {} + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::Storage::connect()?; + /// match device.enable_encrypted_volume("123456") { + /// Ok(()) => { + /// println!("Enabled the encrypted volume."); + /// use_volume(); + /// match device.disable_encrypted_volume() { + /// Ok(()) => println!("Disabled the encrypted volume."), + /// Err(err) => { + /// println!("Could not disable the encrypted volume: {}", err); + /// }, + /// }; + /// }, + /// Err(err) => println!("Could not enable the encrypted volume: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + pub fn disable_encrypted_volume(&self) -> Result<(), CommandError> { + unsafe { get_command_result(nitrokey_sys::NK_lock_encrypted_volume()) } + } + + + /// Returns the status of the connected storage device. + /// + /// # Example + /// + /// ```no_run + /// # use nitrokey::CommandError; + /// + /// fn use_volume() {} + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::Storage::connect()?; + /// match device.get_status() { + /// Ok(status) => { + /// println!("SD card ID: {:#x}", status.serial_number_sd_card); + /// }, + /// Err(err) => println!("Could not get Storage status: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + pub fn get_status(&self) -> Result<StorageStatus, CommandError> { + let mut raw_status = nitrokey_sys::NK_storage_status { + unencrypted_volume_read_only: false, + unencrypted_volume_active: false, + encrypted_volume_read_only: false, + encrypted_volume_active: false, + hidden_volume_read_only: false, + hidden_volume_active: false, + firmware_version_major: 0, + firmware_version_minor: 0, + firmware_locked: false, + serial_number_sd_card: 0, + serial_number_smart_card: 0, + user_retry_count: 0, + admin_retry_count: 0, + new_sd_card_found: false, + filled_with_random: false, + stick_initialized: false, + }; + let raw_result = unsafe { nitrokey_sys::NK_get_status_storage(&mut raw_status) }; + let result = get_command_result(raw_result); + result.and(Ok(StorageStatus::from(raw_status))) + } +} + +impl Drop for Storage { + fn drop(&mut self) { + unsafe { + nitrokey_sys::NK_logout(); + } + } +} + +impl Device for Storage {} + +impl GenerateOtp for Storage {} + +impl From<nitrokey_sys::NK_storage_status> for StorageStatus { + fn from(status: nitrokey_sys::NK_storage_status) -> Self { + StorageStatus { + unencrypted_volume: VolumeStatus { + read_only: status.unencrypted_volume_read_only, + active: status.unencrypted_volume_active, + }, + encrypted_volume: VolumeStatus { + read_only: status.encrypted_volume_read_only, + active: status.encrypted_volume_active, + }, + hidden_volume: VolumeStatus { + read_only: status.hidden_volume_read_only, + active: status.hidden_volume_active, + }, + firmware_version_major: status.firmware_version_major, + firmware_version_minor: status.firmware_version_minor, + firmware_locked: status.firmware_locked, + serial_number_sd_card: status.serial_number_sd_card, + serial_number_smart_card: status.serial_number_smart_card, + user_retry_count: status.user_retry_count, + admin_retry_count: status.admin_retry_count, + new_sd_card_found: status.new_sd_card_found, + filled_with_random: status.filled_with_random, + stick_initialized: status.stick_initialized, + } + } +} diff --git a/nitrokey/src/lib.rs b/nitrokey/src/lib.rs new file mode 100644 index 0000000..e70aa73 --- /dev/null +++ b/nitrokey/src/lib.rs @@ -0,0 +1,127 @@ +//! Provides access to a Nitrokey device using the native libnitrokey API. +//! +//! # Usage +//! +//! Operations on the Nitrokey require different authentication levels. Some operations can be +//! performed without authentication, some require user access, and some require admin access. +//! This is modelled using the types [`User`][] and [`Admin`][]. +//! +//! Use [`connect`][] to connect to any Nitrokey device. The method will return a +//! [`DeviceWrapper`][] that abstracts over the supported Nitrokey devices. You can also use +//! [`Pro::connect`][] or [`Storage::connect`][] to connect to a specific device. +//! +//! You can then use [`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. +//! +//! This makes sure that you can only execute a command if you have the required access rights. +//! Otherwise, your code will not compile. The only exception are the methods to generate one-time +//! passwords – [`get_hotp_code`][] and [`get_totp_code`][]. Depending on the stick configuration, +//! these operations are available without authentication or with user authentication. +//! +//! # Examples +//! +//! Connect to any Nitrokey and print its serial number: +//! +//! ```no_run +//! use nitrokey::Device; +//! # use nitrokey::CommandError; +//! +//! # fn try_main() -> Result<(), CommandError> { +//! let device = nitrokey::connect()?; +//! println!("{}", device.get_serial_number()?); +//! # Ok(()) +//! # } +//! ``` +//! +//! Configure an HOTP slot: +//! +//! ```no_run +//! use nitrokey::{Authenticate, ConfigureOtp, OtpMode, OtpSlotData}; +//! # use nitrokey::CommandError; +//! +//! # fn try_main() -> Result<(), (CommandError)> { +//! let device = nitrokey::connect()?; +//! let slot_data = OtpSlotData::new(1, "test", "01234567890123456689", OtpMode::SixDigits); +//! match device.authenticate_admin("12345678") { +//! Ok(admin) => { +//! match admin.write_hotp_slot(slot_data, 0) { +//! Ok(()) => println!("Successfully wrote slot."), +//! Err(err) => println!("Could not write slot: {}", err), +//! } +//! }, +//! Err((_, err)) => println!("Could not authenticate as admin: {}", err), +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! Generate an HOTP one-time password: +//! +//! ```no_run +//! use nitrokey::{Device, GenerateOtp}; +//! # use nitrokey::CommandError; +//! +//! # fn try_main() -> Result<(), (CommandError)> { +//! let device = nitrokey::connect()?; +//! match device.get_hotp_code(1) { +//! Ok(code) => println!("Generated HOTP code: {}", code), +//! Err(err) => println!("Could not generate HOTP code: {}", err), +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! [`authenticate_admin`]: trait.Authenticate.html#method.authenticate_admin +//! [`authenticate_user`]: trait.Authenticate.html#method.authenticate_user +//! [`connect`]: fn.connect.html +//! [`Pro::connect`]: struct.Pro.html#fn.connect.html +//! [`Storage::connect`]: struct.Storage.html#fn.connect.html +//! [`device`]: struct.User.html#method.device +//! [`get_hotp_code`]: trait.GenerateOtp.html#method.get_hotp_code +//! [`get_totp_code`]: trait.GenerateOtp.html#method.get_totp_code +//! [`Admin`]: struct.Admin.html +//! [`DeviceWrapper`]: enum.DeviceWrapper.html +//! [`User`]: struct.User.html + +extern crate libc; +extern crate nitrokey_sys; +extern crate rand; + +mod auth; +mod config; +mod device; +mod otp; +mod pws; +#[cfg(test)] +mod tests; +mod util; + +pub use auth::{Admin, Authenticate, User}; +pub use config::Config; +pub use device::{connect, Device, DeviceWrapper, Pro, Storage, StorageStatus, VolumeStatus}; +pub use otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; +pub use pws::{GetPasswordSafe, PasswordSafe, SLOT_COUNT}; +pub use util::{CommandError, LogLevel}; + +/// 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`][]). +/// +/// If debug output is enabled, detailed information about the communication with the Nitrokey +/// device is printed to the standard output. +/// +/// [`set_log_level`]: fn.set_log_level.html +pub fn set_debug(state: bool) { + unsafe { + nitrokey_sys::NK_set_debug(state); + } +} + +/// Sets the log level for libnitrokey. All log messages are written to the standard error stream. +/// Setting the log level enables all log messages on the same or on a higher log level. +pub fn set_log_level(level: LogLevel) { + unsafe { + nitrokey_sys::NK_set_debug_level(level.into()); + } +} diff --git a/nitrokey/src/otp.rs b/nitrokey/src/otp.rs new file mode 100644 index 0000000..00a5e5e --- /dev/null +++ b/nitrokey/src/otp.rs @@ -0,0 +1,407 @@ +use nitrokey_sys; +use std::ffi::CString; +use util::{get_command_result, get_cstring, result_from_string, CommandError}; + +/// Modes for one-time password generation. +#[derive(Debug, PartialEq)] +pub enum OtpMode { + /// Generate one-time passwords with six digits. + SixDigits, + /// Generate one-time passwords with eight digits. + EightDigits, +} + +/// Provides methods to configure and erase OTP slots on a Nitrokey device. +pub trait ConfigureOtp { + /// Configure an HOTP slot with the given data and set the HOTP counter to the given value + /// (default 0). + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if there is no slot with the given number + /// - [`InvalidString`][] if the provided token ID contains a null byte + /// - [`NoName`][] if the provided name is empty + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{Authenticate, ConfigureOtp, OtpMode, OtpSlotData}; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), (CommandError)> { + /// let device = nitrokey::connect()?; + /// let slot_data = OtpSlotData::new(1, "test", "01234567890123456689", OtpMode::SixDigits); + /// match device.authenticate_admin("12345678") { + /// Ok(admin) => { + /// match admin.write_hotp_slot(slot_data, 0) { + /// Ok(()) => println!("Successfully wrote slot."), + /// Err(err) => println!("Could not write slot: {}", err), + /// } + /// }, + /// Err((_, err)) => println!("Could not authenticate as admin: {}", err), + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`NoName`]: enum.CommandError.html#variant.NoName + fn write_hotp_slot(&self, data: OtpSlotData, counter: u64) -> Result<(), CommandError>; + + /// Configure a TOTP slot with the given data and set the TOTP time window to the given value + /// (default 30). + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if there is no slot with the given number + /// - [`InvalidString`][] if the provided token ID contains a null byte + /// - [`NoName`][] if the provided name is empty + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{Authenticate, ConfigureOtp, OtpMode, OtpSlotData}; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), (CommandError)> { + /// let device = nitrokey::connect()?; + /// let slot_data = OtpSlotData::new(1, "test", "01234567890123456689", OtpMode::EightDigits); + /// match device.authenticate_admin("12345678") { + /// Ok(admin) => { + /// match admin.write_totp_slot(slot_data, 30) { + /// Ok(()) => println!("Successfully wrote slot."), + /// Err(err) => println!("Could not write slot: {}", err), + /// } + /// }, + /// Err((_, err)) => println!("Could not authenticate as admin: {}", err), + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`NoName`]: enum.CommandError.html#variant.NoName + fn write_totp_slot(&self, data: OtpSlotData, time_window: u16) -> Result<(), CommandError>; + + /// Erases an HOTP slot. + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if there is no slot with the given number + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{Authenticate, ConfigureOtp}; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), (CommandError)> { + /// let device = nitrokey::connect()?; + /// match device.authenticate_admin("12345678") { + /// Ok(admin) => { + /// match admin.erase_hotp_slot(1) { + /// Ok(()) => println!("Successfully erased slot."), + /// Err(err) => println!("Could not erase slot: {}", err), + /// } + /// }, + /// Err((_, err)) => println!("Could not authenticate as admin: {}", err), + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + fn erase_hotp_slot(&self, slot: u8) -> Result<(), CommandError>; + + /// Erases a TOTP slot. + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if there is no slot with the given number + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{Authenticate, ConfigureOtp}; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), (CommandError)> { + /// let device = nitrokey::connect()?; + /// match device.authenticate_admin("12345678") { + /// Ok(admin) => { + /// match admin.erase_totp_slot(1) { + /// Ok(()) => println!("Successfully erased slot."), + /// Err(err) => println!("Could not erase slot: {}", err), + /// } + /// }, + /// Err((_, err)) => println!("Could not authenticate as admin: {}", err), + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + fn erase_totp_slot(&self, slot: u8) -> Result<(), CommandError>; +} + +/// Provides methods to generate OTP codes and to query OTP slots on a Nitrokey +/// device. +pub trait GenerateOtp { + /// Sets the time on the Nitrokey. This command may set the time to arbitrary values. `time` + /// is the number of seconds since January 1st, 1970 (Unix timestamp). + /// + /// The time is used for TOTP generation (see [`get_totp_code`][]). + /// + /// # Example + /// + /// ```ignore + /// extern crate chrono; + /// + /// use chrono::Utc; + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let time = Utc::now().timestamp(); + /// if time < 0 { + /// println!("Timestamps before 1970-01-01 are not supported!"); + /// } else { + /// device.set_time(time as u64); + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// - [`Timestamp`][] if the time could not be set + /// + /// [`get_totp_code`]: #method.get_totp_code + /// [`Timestamp`]: enum.CommandError.html#variant.Timestamp + fn set_time(&self, time: u64) -> Result<(), CommandError> { + unsafe { get_command_result(nitrokey_sys::NK_totp_set_time(time)) } + } + + /// Returns the name of the given HOTP slot. + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if there is no slot with the given number + /// - [`SlotNotProgrammed`][] if the given slot is not configured + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{CommandError, GenerateOtp}; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.get_hotp_slot_name(1) { + /// Ok(name) => println!("HOTP slot 1: {}", name), + /// Err(CommandError::SlotNotProgrammed) => println!("HOTP slot 1 not programmed"), + /// Err(err) => println!("Could not get slot name: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed + fn get_hotp_slot_name(&self, slot: u8) -> Result<String, CommandError> { + unsafe { result_from_string(nitrokey_sys::NK_get_hotp_slot_name(slot)) } + } + + /// Returns the name of the given TOTP slot. + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if there is no slot with the given number + /// - [`SlotNotProgrammed`][] if the given slot is not configured + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{CommandError, GenerateOtp}; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.get_totp_slot_name(1) { + /// Ok(name) => println!("TOTP slot 1: {}", name), + /// Err(CommandError::SlotNotProgrammed) => println!("TOTP slot 1 not programmed"), + /// Err(err) => println!("Could not get slot name: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed + fn get_totp_slot_name(&self, slot: u8) -> Result<String, CommandError> { + unsafe { result_from_string(nitrokey_sys::NK_get_totp_slot_name(slot)) } + } + + /// Generates an HOTP code on the given slot. This operation may require user authorization, + /// depending on the device configuration (see [`get_config`][]). + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if there is no slot with the given number + /// - [`NotAuthorized`][] if OTP generation requires user authentication + /// - [`SlotNotProgrammed`][] if the given slot is not configured + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::GenerateOtp; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let code = device.get_hotp_code(1)?; + /// println!("Generated HOTP code on slot 1: {}", code); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`get_config`]: trait.Device.html#method.get_config + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized + /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed + fn get_hotp_code(&self, slot: u8) -> Result<String, CommandError> { + unsafe { + return result_from_string(nitrokey_sys::NK_get_hotp_code(slot)); + } + } + + /// Generates a TOTP code on the given slot. This operation may require user authorization, + /// depending on the device configuration (see [`get_config`][]). + /// + /// To make sure that the Nitrokey’s time is in sync, consider calling [`set_time`][] before + /// calling this method. + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if there is no slot with the given number + /// - [`NotAuthorized`][] if OTP generation requires user authentication + /// - [`SlotNotProgrammed`][] if the given slot is not configured + /// + /// # Example + /// + /// ```ignore + /// extern crate chrono; + /// + /// use nitrokey::GenerateOtp; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let time = Utc::now().timestamp(); + /// if time < 0 { + /// println!("Timestamps before 1970-01-01 are not supported!"); + /// } else { + /// device.set_time(time as u64); + /// let code = device.get_totp_code(1)?; + /// println!("Generated TOTP code on slot 1: {}", code); + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// [`set_time`]: #method.set_time + /// [`get_config`]: trait.Device.html#method.get_config + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized + /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed + fn get_totp_code(&self, slot: u8) -> Result<String, CommandError> { + unsafe { + return result_from_string(nitrokey_sys::NK_get_totp_code(slot, 0, 0, 0)); + } + } +} + +/// The configuration for an OTP slot. +#[derive(Debug)] +pub struct OtpSlotData { + /// The number of the slot – must be less than three for HOTP and less than 15 for TOTP. + pub number: u8, + /// The name of the slot – must not be empty. + pub name: String, + /// The secret for the slot. + pub secret: String, + /// The OTP generation mode. + pub mode: OtpMode, + /// If true, press the enter key after sending an OTP code using double-pressed + /// numlock, capslock or scrolllock. + pub use_enter: bool, + /// Set the token ID, see [OATH Token Identifier Specification][tokspec], section “Class A”. + /// + /// [tokspec]: https://openauthentication.org/token-specs/ + pub token_id: Option<String>, +} + +#[derive(Debug)] +pub struct RawOtpSlotData { + pub number: u8, + pub name: CString, + pub secret: CString, + pub mode: OtpMode, + pub use_enter: bool, + pub use_token_id: bool, + pub token_id: CString, +} + +impl OtpSlotData { + /// Constructs a new instance of this struct. + pub fn new<S: Into<String>, T: Into<String>>( + number: u8, + name: S, + secret: T, + mode: OtpMode, + ) -> OtpSlotData { + OtpSlotData { + number, + name: name.into(), + secret: secret.into(), + mode, + use_enter: false, + token_id: None, + } + } + + /// Enables pressing the enter key after sending an OTP code using double-pressed numlock, + /// capslock or scrollock. + pub fn use_enter(mut self) -> OtpSlotData { + self.use_enter = true; + self + } + + /// Sets the token ID, see [OATH Token Identifier Specification][tokspec], section “Class A”. + /// + /// [tokspec]: https://openauthentication.org/token-specs/ + pub fn token_id<S: Into<String>>(mut self, id: S) -> OtpSlotData { + self.token_id = Some(id.into()); + self + } +} + +impl RawOtpSlotData { + pub fn new(data: OtpSlotData) -> Result<RawOtpSlotData, CommandError> { + let name = get_cstring(data.name)?; + let secret = get_cstring(data.secret)?; + let use_token_id = data.token_id.is_some(); + let token_id = get_cstring(data.token_id.unwrap_or_else(String::new))?; + + Ok(RawOtpSlotData { + number: data.number, + name, + secret, + mode: data.mode, + use_enter: data.use_enter, + use_token_id, + token_id, + }) + } +} diff --git a/nitrokey/src/pws.rs b/nitrokey/src/pws.rs new file mode 100644 index 0000000..c20fe9d --- /dev/null +++ b/nitrokey/src/pws.rs @@ -0,0 +1,351 @@ +use device::{Device, DeviceWrapper, Pro, Storage}; +use libc; +use nitrokey_sys; +use util::{get_command_result, get_cstring, get_last_error, result_from_string, CommandError}; + +/// The number of slots in a [`PasswordSafe`][]. +/// +/// [`PasswordSafe`]: struct.PasswordSafe.html +pub const SLOT_COUNT: u8 = 16; + +/// A password safe on a Nitrokey device. +/// +/// The password safe stores a tuple consisting of a name, a login and a password on a slot. The +/// number of available slots is [`SLOT_COUNT`][]. The slots are addressed starting with zero. To +/// retrieve a password safe from a Nitrokey device, use the [`get_password_safe`][] method from +/// the [`GetPasswordSafe`][] trait. Note that the device must live at least as long as the +/// password safe. +/// +/// Once the password safe has been unlocked, it can be accessed without a password. Therefore it +/// is mandatory to call [`lock`][] on the corresponding device after the password store is used. +/// As this command may have side effects on the Nitrokey Storage, it cannot be called +/// automatically once the password safe is destroyed. +/// +/// # Examples +/// +/// Open a password safe and access a password: +/// +/// ```no_run +/// use nitrokey::{Device, GetPasswordSafe, PasswordSafe}; +/// # use nitrokey::CommandError; +/// +/// fn use_password_safe(pws: &PasswordSafe) -> Result<(), CommandError> { +/// let name = pws.get_slot_name(0)?; +/// let login = pws.get_slot_login(0)?; +/// let password = pws.get_slot_login(0)?; +/// println!("Credentials for {}: login {}, password {}", name, login, password); +/// Ok(()) +/// } +/// +/// # fn try_main() -> Result<(), CommandError> { +/// let device = nitrokey::connect()?; +/// let pws = device.get_password_safe("123456")?; +/// use_password_safe(&pws); +/// device.lock()?; +/// # Ok(()) +/// # } +/// ``` +/// +/// [`SLOT_COUNT`]: constant.SLOT_COUNT.html +/// [`get_password_safe`]: trait.GetPasswordSafe.html#method.get_password_safe +/// [`lock`]: trait.Device.html#method.lock +/// [`GetPasswordSafe`]: trait.GetPasswordSafe.html +pub struct PasswordSafe<'a> { + _device: &'a Device, +} + +/// Provides access to a [`PasswordSafe`][]. +/// +/// The device that implements this trait must always live at least as long as a password safe +/// retrieved from it. +/// +/// [`PasswordSafe`]: struct.PasswordSafe.html +pub trait GetPasswordSafe { + /// Enables and returns the password safe. + /// + /// The underlying device must always live at least as long as a password safe retrieved from + /// it. It is mandatory to lock the underlying device using [`lock`][] after the password safe + /// has been used. Otherwise, other applications can access the password store without + /// authentication. + /// + /// # Errors + /// + /// - [`InvalidString`][] if one of the provided passwords contains a null byte + /// - [`WrongPassword`][] if the current user password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{Device, GetPasswordSafe, PasswordSafe}; + /// # use nitrokey::CommandError; + /// + /// fn use_password_safe(pws: &PasswordSafe) {} + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.get_password_safe("123456") { + /// Ok(pws) => { + /// use_password_safe(&pws); + /// device.lock()?; + /// }, + /// Err(err) => println!("Could not open the password safe: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`device`]: struct.PasswordSafe.html#method.device + /// [`lock`]: trait.Device.html#method.lock + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + fn get_password_safe(&self, user_pin: &str) -> Result<PasswordSafe, CommandError>; +} + +fn get_password_safe<'a>( + device: &'a Device, + user_pin: &str, +) -> Result<PasswordSafe<'a>, CommandError> { + let user_pin_string = get_cstring(user_pin)?; + let result = unsafe { + get_command_result(nitrokey_sys::NK_enable_password_safe( + user_pin_string.as_ptr(), + )) + }; + result.map(|()| PasswordSafe { _device: device }) +} + +impl<'a> PasswordSafe<'a> { + /// Returns the status of all password slots. + /// + /// The status indicates whether a slot is programmed or not. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{GetPasswordSafe, SLOT_COUNT}; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let pws = device.get_password_safe("123456")?; + /// pws.get_slot_status()?.iter().enumerate().for_each(|(slot, programmed)| { + /// let status = match *programmed { + /// true => "programmed", + /// false => "not programmed", + /// }; + /// println!("Slot {}: {}", slot, status); + /// }); + /// # Ok(()) + /// # } + /// ``` + pub fn get_slot_status(&self) -> Result<[bool; SLOT_COUNT as usize], CommandError> { + let status_ptr = unsafe { nitrokey_sys::NK_get_password_safe_slot_status() }; + if status_ptr.is_null() { + return Err(get_last_error()); + } + let status_array_ptr = status_ptr as *const [u8; SLOT_COUNT as usize]; + let status_array = unsafe { *status_array_ptr }; + let mut result = [false; SLOT_COUNT as usize]; + for i in 0..SLOT_COUNT { + result[i as usize] = status_array[i as usize] == 1; + } + unsafe { + libc::free(status_ptr as *mut libc::c_void); + } + Ok(result) + } + + /// Returns the name of the given slot (if it is programmed). + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if the given slot is out of range + /// - [`Unknown`][] if the slot is not programmed + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::GetPasswordSafe; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.get_password_safe("123456") { + /// Ok(pws) => { + /// let name = pws.get_slot_name(0)?; + /// let login = pws.get_slot_login(0)?; + /// let password = pws.get_slot_login(0)?; + /// println!("Credentials for {}: login {}, password {}", name, login, password); + /// }, + /// Err(err) => println!("Could not open the password safe: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + /// [`Unknown`]: enum.CommandError.html#variant.Unknown + pub fn get_slot_name(&self, slot: u8) -> Result<String, CommandError> { + unsafe { result_from_string(nitrokey_sys::NK_get_password_safe_slot_name(slot)) } + } + + /// Returns the login for the given slot (if it is programmed). + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if the given slot is out of range + /// - [`Unknown`][] if the slot is not programmed + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::GetPasswordSafe; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let pws = device.get_password_safe("123456")?; + /// let name = pws.get_slot_name(0)?; + /// let login = pws.get_slot_login(0)?; + /// let password = pws.get_slot_login(0)?; + /// println!("Credentials for {}: login {}, password {}", name, login, password); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + /// [`Unknown`]: enum.CommandError.html#variant.Unknown + pub fn get_slot_login(&self, slot: u8) -> Result<String, CommandError> { + unsafe { result_from_string(nitrokey_sys::NK_get_password_safe_slot_login(slot)) } + } + + /// Returns the password for the given slot (if it is programmed). + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if the given slot is out of range + /// - [`Unknown`][] if the slot is not programmed + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::GetPasswordSafe; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let pws = device.get_password_safe("123456")?; + /// let name = pws.get_slot_name(0)?; + /// let login = pws.get_slot_login(0)?; + /// let password = pws.get_slot_login(0)?; + /// println!("Credentials for {}: login {}, password {}", name, login, password); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + /// [`Unknown`]: enum.CommandError.html#variant.Unknown + pub fn get_slot_password(&self, slot: u8) -> Result<String, CommandError> { + unsafe { result_from_string(nitrokey_sys::NK_get_password_safe_slot_password(slot)) } + } + + /// Writes the given slot with the given name, login and password. + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if the given slot is out of range + /// - [`InvalidString`][] if the provided token ID contains a null byte + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::GetPasswordSafe; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let pws = device.get_password_safe("123456")?; + /// let name = pws.get_slot_name(0)?; + /// let login = pws.get_slot_login(0)?; + /// let password = pws.get_slot_login(0)?; + /// println!("Credentials for {}: login {}, password {}", name, login, password); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + pub fn write_slot( + &self, + slot: u8, + name: &str, + login: &str, + password: &str, + ) -> Result<(), CommandError> { + let name_string = get_cstring(name)?; + let login_string = get_cstring(login)?; + let password_string = get_cstring(password)?; + unsafe { + get_command_result(nitrokey_sys::NK_write_password_safe_slot( + slot, + name_string.as_ptr(), + login_string.as_ptr(), + password_string.as_ptr(), + )) + } + } + + /// Erases the given slot. Erasing clears the stored name, login and password (if the slot was + /// programmed). + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if the given slot is out of range + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::GetPasswordSafe; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let pws = device.get_password_safe("123456")?; + /// match pws.erase_slot(0) { + /// Ok(()) => println!("Erased slot 0."), + /// Err(err) => println!("Could not erase slot 0: {}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + pub fn erase_slot(&self, slot: u8) -> Result<(), CommandError> { + unsafe { get_command_result(nitrokey_sys::NK_erase_password_safe_slot(slot)) } + } +} + +impl<'a> Drop for PasswordSafe<'a> { + fn drop(&mut self) { + // TODO: disable the password safe -- NK_lock_device has side effects on the Nitrokey + // Storage, see https://github.com/Nitrokey/nitrokey-storage-firmware/issues/65 + } +} + +impl GetPasswordSafe for Pro { + fn get_password_safe(&self, user_pin: &str) -> Result<PasswordSafe, CommandError> { + get_password_safe(self, user_pin) + } +} + +impl GetPasswordSafe for Storage { + fn get_password_safe(&self, user_pin: &str) -> Result<PasswordSafe, CommandError> { + get_password_safe(self, user_pin) + } +} + +impl GetPasswordSafe for DeviceWrapper { + fn get_password_safe(&self, user_pin: &str) -> Result<PasswordSafe, CommandError> { + get_password_safe(self, user_pin) + } +} diff --git a/nitrokey/src/tests/device.rs b/nitrokey/src/tests/device.rs new file mode 100644 index 0000000..fed465d --- /dev/null +++ b/nitrokey/src/tests/device.rs @@ -0,0 +1,304 @@ +use std::ffi::CStr; +use std::process::Command; +use std::{thread, time}; +use tests::util::{Target, ADMIN_PASSWORD, USER_PASSWORD}; +use {Authenticate, CommandError, Config, Device, Storage}; + +static ADMIN_NEW_PASSWORD: &str = "1234567890"; +static USER_NEW_PASSWORD: &str = "abcdefghij"; + +fn count_nitrokey_block_devices() -> usize { + thread::sleep(time::Duration::from_secs(2)); + let output = Command::new("lsblk") + .args(&["-o", "MODEL"]) + .output() + .expect("Could not list block devices"); + String::from_utf8_lossy(&output.stdout) + .split("\n") + .filter(|&s| s == "Nitrokey Storage") + .count() +} + +#[test] +#[cfg_attr(not(feature = "test-no-device"), ignore)] +fn connect_no_device() { + assert!(::connect().is_err()); + assert!(::Pro::connect().is_err()); + assert!(::Storage::connect().is_err()); +} + +#[test] +#[cfg_attr(not(feature = "test-pro"), ignore)] +fn connect_pro() { + assert!(::connect().is_ok()); + assert!(::Pro::connect().is_ok()); + assert!(::Storage::connect().is_err()); + match ::connect().unwrap() { + ::DeviceWrapper::Pro(_) => assert!(true), + ::DeviceWrapper::Storage(_) => assert!(false), + }; +} + +#[test] +#[cfg_attr(not(feature = "test-storage"), ignore)] +fn connect_storage() { + assert!(::connect().is_ok()); + assert!(::Pro::connect().is_err()); + assert!(::Storage::connect().is_ok()); + match ::connect().unwrap() { + ::DeviceWrapper::Pro(_) => assert!(false), + ::DeviceWrapper::Storage(_) => assert!(true), + }; +} + +fn assert_empty_serial_number() { + unsafe { + let ptr = ::nitrokey_sys::NK_device_serial_number(); + assert!(!ptr.is_null()); + let cstr = CStr::from_ptr(ptr); + assert_eq!(cstr.to_string_lossy(), ""); + } +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn disconnect() { + Target::connect().unwrap(); + assert_empty_serial_number(); + Target::connect() + .unwrap() + .authenticate_admin(ADMIN_PASSWORD) + .unwrap(); + assert_empty_serial_number(); + Target::connect() + .unwrap() + .authenticate_user(USER_PASSWORD) + .unwrap(); + assert_empty_serial_number(); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn get_serial_number() { + let device = Target::connect().unwrap(); + let result = device.get_serial_number(); + assert!(result.is_ok()); + let serial_number = result.unwrap(); + assert!(serial_number.is_ascii()); + assert!(serial_number.chars().all(|c| c.is_ascii_hexdigit())); +} +#[test] +#[cfg_attr(not(feature = "test-pro"), ignore)] +fn get_firmware_version() { + let device = Target::connect().unwrap(); + assert_eq!(0, device.get_major_firmware_version()); + let minor = device.get_minor_firmware_version(); + assert!(minor > 0); +} + +fn admin_retry<T: Authenticate + Device>(device: T, suffix: &str, count: u8) -> T { + let result = device.authenticate_admin(&(ADMIN_PASSWORD.to_owned() + suffix)); + let device = match result { + Ok(admin) => admin.device(), + Err((device, _)) => device, + }; + assert_eq!(count, device.get_admin_retry_count()); + return device; +} + +fn user_retry<T: Authenticate + Device>(device: T, suffix: &str, count: u8) -> T { + let result = device.authenticate_user(&(USER_PASSWORD.to_owned() + suffix)); + let device = match result { + Ok(admin) => admin.device(), + Err((device, _)) => device, + }; + assert_eq!(count, device.get_user_retry_count()); + return device; +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn get_retry_count() { + let device = Target::connect().unwrap(); + + let device = admin_retry(device, "", 3); + let device = admin_retry(device, "123", 2); + let device = admin_retry(device, "456", 1); + let device = admin_retry(device, "", 3); + + let device = user_retry(device, "", 3); + let device = user_retry(device, "123", 2); + let device = user_retry(device, "456", 1); + user_retry(device, "", 3); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn config() { + let device = Target::connect().unwrap(); + let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap(); + let config = Config::new(None, None, None, true); + assert!(admin.write_config(config).is_ok()); + let get_config = admin.get_config().unwrap(); + assert_eq!(config, get_config); + + let config = Config::new(None, Some(9), None, true); + assert_eq!(Err(CommandError::InvalidSlot), admin.write_config(config)); + + let config = Config::new(Some(1), None, Some(0), false); + assert!(admin.write_config(config).is_ok()); + let get_config = admin.get_config().unwrap(); + assert_eq!(config, get_config); + + let config = Config::new(None, None, None, false); + assert!(admin.write_config(config).is_ok()); + let get_config = admin.get_config().unwrap(); + assert_eq!(config, get_config); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn change_user_pin() { + let device = Target::connect().unwrap(); + let device = device.authenticate_user(USER_PASSWORD).unwrap().device(); + let device = device.authenticate_user(USER_NEW_PASSWORD).unwrap_err().0; + + assert!( + device + .change_user_pin(USER_PASSWORD, USER_NEW_PASSWORD) + .is_ok() + ); + + let device = device.authenticate_user(USER_PASSWORD).unwrap_err().0; + let device = device + .authenticate_user(USER_NEW_PASSWORD) + .unwrap() + .device(); + + let result = device.change_user_pin(USER_PASSWORD, USER_PASSWORD); + assert_eq!(Err(CommandError::WrongPassword), result); + + assert!( + device + .change_user_pin(USER_NEW_PASSWORD, USER_PASSWORD) + .is_ok() + ); + + let device = device.authenticate_user(USER_PASSWORD).unwrap().device(); + assert!(device.authenticate_user(USER_NEW_PASSWORD).is_err()); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn change_admin_pin() { + let device = Target::connect().unwrap(); + let device = device.authenticate_admin(ADMIN_PASSWORD).unwrap().device(); + let device = device.authenticate_admin(ADMIN_NEW_PASSWORD).unwrap_err().0; + + assert!( + device + .change_admin_pin(ADMIN_PASSWORD, ADMIN_NEW_PASSWORD) + .is_ok() + ); + + let device = device.authenticate_admin(ADMIN_PASSWORD).unwrap_err().0; + let device = device + .authenticate_admin(ADMIN_NEW_PASSWORD) + .unwrap() + .device(); + + assert_eq!( + Err(CommandError::WrongPassword), + device.change_admin_pin(ADMIN_PASSWORD, ADMIN_PASSWORD) + ); + + assert!( + device + .change_admin_pin(ADMIN_NEW_PASSWORD, ADMIN_PASSWORD) + .is_ok() + ); + + let device = device.authenticate_admin(ADMIN_PASSWORD).unwrap().device(); + device.authenticate_admin(ADMIN_NEW_PASSWORD).unwrap_err(); +} + +fn require_failed_user_login(device: Target, password: &str, error: CommandError) -> Target { + let result = device.authenticate_user(password); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(error, err.1); + err.0 +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn unlock_user_pin() { + let device = Target::connect().unwrap(); + let device = device.authenticate_user(USER_PASSWORD).unwrap().device(); + assert!( + device + .unlock_user_pin(ADMIN_PASSWORD, USER_PASSWORD) + .is_ok() + ); + assert_eq!( + Err(CommandError::WrongPassword), + device.unlock_user_pin(USER_PASSWORD, USER_PASSWORD) + ); + + let wrong_password = USER_PASSWORD.to_owned() + "foo"; + let device = require_failed_user_login(device, &wrong_password, CommandError::WrongPassword); + let device = require_failed_user_login(device, &wrong_password, CommandError::WrongPassword); + let device = require_failed_user_login(device, &wrong_password, CommandError::WrongPassword); + let device = require_failed_user_login(device, USER_PASSWORD, CommandError::WrongPassword); + + assert_eq!( + Err(CommandError::WrongPassword), + device.unlock_user_pin(USER_PASSWORD, USER_PASSWORD) + ); + assert!( + device + .unlock_user_pin(ADMIN_PASSWORD, USER_PASSWORD) + .is_ok() + ); + device.authenticate_user(USER_PASSWORD).unwrap(); +} + +#[test] +#[cfg_attr(not(feature = "test-storage"), ignore)] +fn encrypted_volume() { + let device = Storage::connect().unwrap(); + assert!(device.lock().is_ok()); + + assert_eq!(1, count_nitrokey_block_devices()); + assert!(device.disable_encrypted_volume().is_ok()); + assert_eq!(1, count_nitrokey_block_devices()); + assert_eq!( + Err(CommandError::WrongPassword), + device.enable_encrypted_volume("123") + ); + assert_eq!(1, count_nitrokey_block_devices()); + assert!(device.enable_encrypted_volume(USER_PASSWORD).is_ok()); + assert_eq!(2, count_nitrokey_block_devices()); + assert!(device.disable_encrypted_volume().is_ok()); + assert_eq!(1, count_nitrokey_block_devices()); +} + +#[test] +#[cfg_attr(not(feature = "test-storage"), ignore)] +fn lock() { + let device = Storage::connect().unwrap(); + + assert!(device.enable_encrypted_volume(USER_PASSWORD).is_ok()); + assert!(device.lock().is_ok()); + assert_eq!(1, count_nitrokey_block_devices()); +} + +#[test] +#[cfg_attr(not(feature = "test-storage"), ignore)] +fn get_storage_status() { + let device = Storage::connect().unwrap(); + let status = device.get_status().unwrap(); + + assert!(status.serial_number_sd_card > 0); + assert!(status.serial_number_smart_card > 0); +} diff --git a/nitrokey/src/tests/mod.rs b/nitrokey/src/tests/mod.rs new file mode 100644 index 0000000..34ca0aa --- /dev/null +++ b/nitrokey/src/tests/mod.rs @@ -0,0 +1,4 @@ +mod device; +mod otp; +mod pws; +mod util; diff --git a/nitrokey/src/tests/otp.rs b/nitrokey/src/tests/otp.rs new file mode 100644 index 0000000..cf71d9d --- /dev/null +++ b/nitrokey/src/tests/otp.rs @@ -0,0 +1,283 @@ +use std::ops::Deref; +use tests::util::{Target, ADMIN_PASSWORD, USER_PASSWORD}; +use {Admin, Authenticate, CommandError, Config, ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; + +// test suite according to RFC 4226, Appendix D +static HOTP_SECRET: &str = "3132333435363738393031323334353637383930"; +static HOTP_CODES: &[&str] = &[ + "755224", "287082", "359152", "969429", "338314", "254676", "287922", "162583", "399871", + "520489", +]; + +// test suite according to RFC 6238, Appendix B +static TOTP_SECRET: &str = "3132333435363738393031323334353637383930"; +static TOTP_CODES: &[(u64, &str)] = &[ + (59, "94287082"), + (1111111109, "07081804"), + (1111111111, "14050471"), + (1234567890, "89005924"), + (2000000000, "69279037"), + (20000000000, "65353130"), +]; + +#[derive(PartialEq)] +enum TotpTimestampSize { + U32, + U64, +} + +fn get_admin_test_device() -> Admin<Target> { + Target::connect() + .expect("Could not connect to the Nitrokey.") + .authenticate_admin(ADMIN_PASSWORD) + .expect("Could not login as admin.") +} + +fn configure_hotp(admin: &ConfigureOtp, counter: u8) { + let slot_data = OtpSlotData::new(1, "test-hotp", HOTP_SECRET, OtpMode::SixDigits); + assert!(admin.write_hotp_slot(slot_data, counter.into()).is_ok()); +} + +fn check_hotp_codes(device: &GenerateOtp, offset: u8) { + HOTP_CODES.iter().enumerate().for_each(|(i, code)| { + if i >= offset as usize { + let result = device.get_hotp_code(1); + assert_eq!(code, &result.unwrap()); + } + }); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn hotp_no_pin() { + let admin = get_admin_test_device(); + let config = Config::new(None, None, None, false); + assert!(admin.write_config(config).is_ok()); + + configure_hotp(&admin, 0); + check_hotp_codes(admin.deref(), 0); + + configure_hotp(&admin, 5); + check_hotp_codes(admin.deref(), 5); + + configure_hotp(&admin, 0); + check_hotp_codes(&admin.device(), 0); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn hotp_pin() { + let admin = get_admin_test_device(); + let config = Config::new(None, None, None, true); + assert!(admin.write_config(config).is_ok()); + + configure_hotp(&admin, 0); + let user = admin.device().authenticate_user(USER_PASSWORD).unwrap(); + check_hotp_codes(&user, 0); + + assert!(user.device().get_hotp_code(1).is_err()); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn hotp_slot_name() { + let admin = get_admin_test_device(); + let slot_data = OtpSlotData::new(1, "test-hotp", HOTP_SECRET, OtpMode::SixDigits); + assert!(admin.write_hotp_slot(slot_data, 0).is_ok()); + + let device = admin.device(); + let result = device.get_hotp_slot_name(1); + assert_eq!("test-hotp", result.unwrap()); + let result = device.get_hotp_slot_name(4); + assert_eq!(CommandError::InvalidSlot, result.unwrap_err()); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn hotp_error() { + let admin = get_admin_test_device(); + let slot_data = OtpSlotData::new(1, "", HOTP_SECRET, OtpMode::SixDigits); + assert_eq!( + Err(CommandError::NoName), + admin.write_hotp_slot(slot_data, 0) + ); + let slot_data = OtpSlotData::new(4, "test", HOTP_SECRET, OtpMode::SixDigits); + assert_eq!( + Err(CommandError::InvalidSlot), + admin.write_hotp_slot(slot_data, 0) + ); + let code = admin.get_hotp_code(4); + assert_eq!(CommandError::InvalidSlot, code.unwrap_err()); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn hotp_erase() { + let admin = get_admin_test_device(); + let config = Config::new(None, None, None, false); + assert!(admin.write_config(config).is_ok()); + let slot_data = OtpSlotData::new(1, "test1", HOTP_SECRET, OtpMode::SixDigits); + assert!(admin.write_hotp_slot(slot_data, 0).is_ok()); + let slot_data = OtpSlotData::new(2, "test2", HOTP_SECRET, OtpMode::SixDigits); + assert!(admin.write_hotp_slot(slot_data, 0).is_ok()); + + assert!(admin.erase_hotp_slot(1).is_ok()); + + let device = admin.device(); + let result = device.get_hotp_slot_name(1); + assert_eq!(CommandError::SlotNotProgrammed, result.unwrap_err()); + let result = device.get_hotp_code(1); + assert_eq!(CommandError::SlotNotProgrammed, result.unwrap_err()); + + assert_eq!("test2", device.get_hotp_slot_name(2).unwrap()); +} + +fn configure_totp(admin: &ConfigureOtp, factor: u64) { + let slot_data = OtpSlotData::new(1, "test-totp", TOTP_SECRET, OtpMode::EightDigits); + let time_window = 30u64.checked_mul(factor).unwrap(); + assert!(admin.write_totp_slot(slot_data, time_window as u16).is_ok()); +} + +fn check_totp_codes(device: &GenerateOtp, factor: u64, timestamp_size: TotpTimestampSize) { + for (i, &(base_time, code)) in TOTP_CODES.iter().enumerate() { + let time = base_time.checked_mul(factor).unwrap(); + let is_u64 = time > u32::max_value() as u64; + if is_u64 != (timestamp_size == TotpTimestampSize::U64) { + continue; + } + + assert!(device.set_time(time).is_ok()); + let result = device.get_totp_code(1); + assert!(result.is_ok()); + let result_code = result.unwrap(); + assert_eq!( + code, result_code, + "TOTP code {} should be {} but is {}", + i, code, result_code + ); + } +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn totp_no_pin() { + // TODO: this test may fail due to bad timing --> find solution + let admin = get_admin_test_device(); + let config = Config::new(None, None, None, false); + assert!(admin.write_config(config).is_ok()); + + configure_totp(&admin, 1); + check_totp_codes(admin.deref(), 1, TotpTimestampSize::U32); + + configure_totp(&admin, 2); + check_totp_codes(admin.deref(), 2, TotpTimestampSize::U32); + + configure_totp(&admin, 1); + check_totp_codes(&admin.device(), 1, TotpTimestampSize::U32); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +#[cfg_attr(feature = "test-storage", should_panic(expected = "assertion failed"))] +// Nitrokey Storage does only support timestamps that fit in a 32-bit unsigned integer. Therefore +// the last RFC test case is expected to fail. +fn totp_no_pin_64() { + let admin = get_admin_test_device(); + let config = Config::new(None, None, None, false); + assert!(admin.write_config(config).is_ok()); + + configure_totp(&admin, 1); + check_totp_codes(admin.deref(), 1, TotpTimestampSize::U64); + + configure_totp(&admin, 2); + check_totp_codes(admin.deref(), 2, TotpTimestampSize::U64); + + configure_totp(&admin, 1); + check_totp_codes(&admin.device(), 1, TotpTimestampSize::U64); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn totp_pin() { + // TODO: this test may fail due to bad timing --> find solution + let admin = get_admin_test_device(); + let config = Config::new(None, None, None, true); + assert!(admin.write_config(config).is_ok()); + + configure_totp(&admin, 1); + let user = admin.device().authenticate_user(USER_PASSWORD).unwrap(); + check_totp_codes(&user, 1, TotpTimestampSize::U32); + + assert!(user.device().get_totp_code(1).is_err()); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +#[cfg_attr(feature = "test-storage", should_panic(expected = "assertion failed"))] +// See comment for totp_no_pin_64. +fn totp_pin_64() { + let admin = get_admin_test_device(); + let config = Config::new(None, None, None, true); + assert!(admin.write_config(config).is_ok()); + + configure_totp(&admin, 1); + let user = admin.device().authenticate_user(USER_PASSWORD).unwrap(); + check_totp_codes(&user, 1, TotpTimestampSize::U64); + + assert!(user.device().get_totp_code(1).is_err()); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn totp_slot_name() { + let admin = get_admin_test_device(); + let slot_data = OtpSlotData::new(1, "test-totp", TOTP_SECRET, OtpMode::EightDigits); + assert!(admin.write_totp_slot(slot_data, 0).is_ok()); + + let device = admin.device(); + let result = device.get_totp_slot_name(1); + assert!(result.is_ok()); + assert_eq!("test-totp", result.unwrap()); + let result = device.get_totp_slot_name(16); + assert_eq!(CommandError::InvalidSlot, result.unwrap_err()); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn totp_error() { + let admin = get_admin_test_device(); + let slot_data = OtpSlotData::new(1, "", HOTP_SECRET, OtpMode::SixDigits); + assert_eq!( + Err(CommandError::NoName), + admin.write_hotp_slot(slot_data, 0) + ); + let slot_data = OtpSlotData::new(4, "test", HOTP_SECRET, OtpMode::SixDigits); + assert_eq!( + Err(CommandError::InvalidSlot), + admin.write_hotp_slot(slot_data, 0) + ); + let code = admin.get_hotp_code(4); + assert_eq!(CommandError::InvalidSlot, code.unwrap_err()); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn totp_erase() { + let admin = get_admin_test_device(); + let config = Config::new(None, None, None, false); + assert!(admin.write_config(config).is_ok()); + let slot_data = OtpSlotData::new(1, "test1", TOTP_SECRET, OtpMode::SixDigits); + assert!(admin.write_totp_slot(slot_data, 0).is_ok()); + let slot_data = OtpSlotData::new(2, "test2", TOTP_SECRET, OtpMode::SixDigits); + assert!(admin.write_totp_slot(slot_data, 0).is_ok()); + + assert!(admin.erase_totp_slot(1).is_ok()); + + let device = admin.device(); + let result = device.get_totp_slot_name(1); + assert_eq!(CommandError::SlotNotProgrammed, result.unwrap_err()); + let result = device.get_totp_code(1); + assert_eq!(CommandError::SlotNotProgrammed, result.unwrap_err()); + + assert_eq!("test2", device.get_totp_slot_name(2).unwrap()); +} diff --git a/nitrokey/src/tests/pws.rs b/nitrokey/src/tests/pws.rs new file mode 100644 index 0000000..f581515 --- /dev/null +++ b/nitrokey/src/tests/pws.rs @@ -0,0 +1,143 @@ +use device::Device; +use nitrokey_sys; +use pws::{GetPasswordSafe, PasswordSafe, SLOT_COUNT}; +use tests::util::{Target, ADMIN_PASSWORD, USER_PASSWORD}; +use util::{result_from_string, CommandError}; + +fn get_pws(device: &Target) -> PasswordSafe { + device.get_password_safe(USER_PASSWORD).unwrap() +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn enable() { + let device = Target::connect().unwrap(); + assert!( + device + .get_password_safe(&(USER_PASSWORD.to_owned() + "123")) + .is_err() + ); + assert!(device.get_password_safe(USER_PASSWORD).is_ok()); + assert!(device.get_password_safe(ADMIN_PASSWORD).is_err()); + assert!(device.get_password_safe(USER_PASSWORD).is_ok()); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn drop() { + let device = Target::connect().unwrap(); + { + let pws = get_pws(&device); + assert!(pws.write_slot(1, "name", "login", "password").is_ok()); + assert_eq!("name", pws.get_slot_name(1).unwrap()); + let result = result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_name(1) }); + assert_eq!(Ok(String::from("name")), result); + } + let result = result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_name(1) }); + assert_eq!(Ok(String::from("name")), result); + assert!(device.lock().is_ok()); + let result = result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_name(1) }); + assert_eq!(Err(CommandError::NotAuthorized), result); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn get_status() { + let device = Target::connect().unwrap(); + let pws = get_pws(&device); + for i in 0..SLOT_COUNT { + assert!(pws.erase_slot(i).is_ok(), "Could not erase slot {}", i); + } + let status = pws.get_slot_status().unwrap(); + assert_eq!(status, [false; SLOT_COUNT as usize]); + + assert!(pws.write_slot(1, "name", "login", "password").is_ok()); + let status = pws.get_slot_status().unwrap(); + for i in 0..SLOT_COUNT { + assert_eq!(i == 1, status[i as usize]); + } + + for i in 0..SLOT_COUNT { + assert!(pws.write_slot(i, "name", "login", "password").is_ok()); + } + let status = pws.get_slot_status().unwrap(); + assert_eq!(status, [true; SLOT_COUNT as usize]); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn get_data() { + let device = Target::connect().unwrap(); + let pws = get_pws(&device); + assert!(pws.write_slot(1, "name", "login", "password").is_ok()); + assert_eq!("name", pws.get_slot_name(1).unwrap()); + assert_eq!("login", pws.get_slot_login(1).unwrap()); + assert_eq!("password", pws.get_slot_password(1).unwrap()); + + assert!(pws.erase_slot(1).is_ok()); + // TODO: check error codes + assert_eq!(Err(CommandError::Unknown), pws.get_slot_name(1)); + assert_eq!(Err(CommandError::Unknown), pws.get_slot_login(1)); + assert_eq!(Err(CommandError::Unknown), pws.get_slot_password(1)); + + let name = "with å"; + let login = "pär@test.com"; + let password = "'i3lJc[09?I:,[u7dWz9"; + assert!(pws.write_slot(1, name, login, password).is_ok()); + assert_eq!(name, pws.get_slot_name(1).unwrap()); + assert_eq!(login, pws.get_slot_login(1).unwrap()); + assert_eq!(password, pws.get_slot_password(1).unwrap()); + + assert_eq!( + Err(CommandError::InvalidSlot), + pws.get_slot_name(SLOT_COUNT) + ); + assert_eq!( + Err(CommandError::InvalidSlot), + pws.get_slot_login(SLOT_COUNT) + ); + assert_eq!( + Err(CommandError::InvalidSlot), + pws.get_slot_password(SLOT_COUNT) + ); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn write() { + let device = Target::connect().unwrap(); + let pws = get_pws(&device); + + assert_eq!( + Err(CommandError::InvalidSlot), + pws.write_slot(SLOT_COUNT, "name", "login", "password") + ); + + assert!(pws.write_slot(0, "", "login", "password").is_ok()); + assert_eq!(Err(CommandError::Unknown), pws.get_slot_name(0)); + assert_eq!(Ok(String::from("login")), pws.get_slot_login(0)); + assert_eq!(Ok(String::from("password")), pws.get_slot_password(0)); + + assert!(pws.write_slot(0, "name", "", "password").is_ok()); + assert_eq!(Ok(String::from("name")), pws.get_slot_name(0)); + assert_eq!(Err(CommandError::Unknown), pws.get_slot_login(0)); + assert_eq!(Ok(String::from("password")), pws.get_slot_password(0)); + + assert!(pws.write_slot(0, "name", "login", "").is_ok()); + assert_eq!(Ok(String::from("name")), pws.get_slot_name(0)); + assert_eq!(Ok(String::from("login")), pws.get_slot_login(0)); + assert_eq!(Err(CommandError::Unknown), pws.get_slot_password(0)); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn erase() { + let device = Target::connect().unwrap(); + let pws = get_pws(&device); + assert_eq!(Err(CommandError::InvalidSlot), pws.erase_slot(SLOT_COUNT)); + + assert!(pws.write_slot(0, "name", "login", "password").is_ok()); + assert!(pws.erase_slot(0).is_ok()); + assert!(pws.erase_slot(0).is_ok()); + assert_eq!(Err(CommandError::Unknown), pws.get_slot_name(0)); +} diff --git a/nitrokey/src/tests/util.rs b/nitrokey/src/tests/util.rs new file mode 100644 index 0000000..c6fbb8f --- /dev/null +++ b/nitrokey/src/tests/util.rs @@ -0,0 +1,11 @@ +pub static ADMIN_PASSWORD: &str = "12345678"; +pub static USER_PASSWORD: &str = "123456"; + +#[cfg(feature = "test-no-device")] +pub type Target = ::Pro; + +#[cfg(feature = "test-pro")] +pub type Target = ::Pro; + +#[cfg(feature = "test-storage")] +pub type Target = ::Storage; diff --git a/nitrokey/src/util.rs b/nitrokey/src/util.rs new file mode 100644 index 0000000..6f4fbb0 --- /dev/null +++ b/nitrokey/src/util.rs @@ -0,0 +1,171 @@ +use libc::{c_void, free}; +use nitrokey_sys; +use rand::{OsRng, Rng}; +use std; +use std::ffi::{CStr, CString}; +use std::fmt; +use std::os::raw::{c_char, c_int}; + +/// Error types returned by Nitrokey device or by the library. +#[derive(Debug, PartialEq)] +pub enum CommandError { + /// A packet with a wrong checksum has been sent or received. + WrongCrc, + /// A command tried to access an OTP slot that does not exist. + WrongSlot, + /// A command tried to generate an OTP on a slot that is not configured. + SlotNotProgrammed, + /// The provided password is wrong. + WrongPassword, + /// You are not authorized for this command or provided a wrong temporary + /// password. + NotAuthorized, + /// An error occured when getting or setting the time. + Timestamp, + /// You did not provide a name for the OTP slot. + NoName, + /// This command is not supported by this device. + NotSupported, + /// This command is unknown. + UnknownCommand, + /// AES decryption failed. + AesDecryptionFailed, + /// An unknown error occured. + Unknown, + /// You passed a string containing a null byte. + InvalidString, + /// You passed an invalid slot. + InvalidSlot, + /// An error occured during random number generation. + RngError, +} + +/// Log level for libnitrokey. +/// +/// Setting the log level to a lower level enables all output from higher levels too. Currently, +/// only the log levels `Warning`, `DebugL1`, `Debug` and `DebugL2` are actually used. +#[derive(Debug, PartialEq)] +pub enum LogLevel { + /// Error messages. Currently not used. + Error, + /// Warning messages. + Warning, + /// Informational messages. Currently not used. + Info, + /// Basic debug messages, especially basic information on the sent and received packets. + DebugL1, + /// Detailed debug messages, especially detailed information on the sent and received packets. + Debug, + /// Very detailed debug messages, especially detailed information about the control flow for + /// device communication (for example function entries and exits). + DebugL2, +} + +pub fn owned_str_from_ptr(ptr: *const c_char) -> String { + unsafe { + return CStr::from_ptr(ptr).to_string_lossy().into_owned(); + } +} + +pub fn result_from_string(ptr: *const c_char) -> Result<String, CommandError> { + if ptr.is_null() { + return Err(CommandError::Unknown); + } + unsafe { + let s = owned_str_from_ptr(ptr); + free(ptr as *mut c_void); + if s.is_empty() { + return Err(get_last_error()); + } + return Ok(s); + } +} + +pub fn get_command_result(value: c_int) -> Result<(), CommandError> { + match value { + 0 => Ok(()), + other => Err(CommandError::from(other)), + } +} + +pub fn get_last_result() -> Result<(), CommandError> { + let value = unsafe { nitrokey_sys::NK_get_last_command_status() } as c_int; + get_command_result(value) +} + +pub fn get_last_error() -> CommandError { + return match get_last_result() { + Ok(()) => CommandError::Unknown, + Err(err) => err, + }; +} + +pub fn generate_password(length: usize) -> std::io::Result<Vec<u8>> { + let mut rng = match OsRng::new() { + Ok(rng) => rng, + Err(err) => return Err(err), + }; + let mut data = vec![0u8; length]; + rng.fill_bytes(&mut data[..]); + return Ok(data); +} + +pub fn get_cstring<T: Into<Vec<u8>>>(s: T) -> Result<CString, CommandError> { + CString::new(s).or(Err(CommandError::InvalidString)) +} + +impl fmt::Display for CommandError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let msg = match *self { + CommandError::WrongCrc => "A packet with a wrong checksum has been sent or received", + CommandError::WrongSlot => "The given OTP slot does not exist", + CommandError::SlotNotProgrammed => "The given OTP slot is not programmed", + CommandError::WrongPassword => "The given password is wrong", + CommandError::NotAuthorized => { + "You are not authorized for this command or provided a wrong temporary password" + } + CommandError::Timestamp => "An error occured when getting or setting the time", + CommandError::NoName => "You did not provide a name for the OTP slot", + CommandError::NotSupported => "This command is not supported by this device", + CommandError::UnknownCommand => "This command is unknown", + CommandError::AesDecryptionFailed => "AES decryption failed", + CommandError::Unknown => "An unknown error occured", + CommandError::InvalidString => "You passed a string containing a null byte", + CommandError::InvalidSlot => "The given slot is invalid", + CommandError::RngError => "An error occured during random number generation", + }; + write!(f, "{}", msg) + } +} + +impl From<c_int> for CommandError { + fn from(value: c_int) -> Self { + match value { + 1 => CommandError::WrongCrc, + 2 => CommandError::WrongSlot, + 3 => CommandError::SlotNotProgrammed, + 4 => CommandError::WrongPassword, + 5 => CommandError::NotAuthorized, + 6 => CommandError::Timestamp, + 7 => CommandError::NoName, + 8 => CommandError::NotSupported, + 9 => CommandError::UnknownCommand, + 10 => CommandError::AesDecryptionFailed, + 201 => CommandError::InvalidSlot, + _ => CommandError::Unknown, + } + } +} + +impl Into<i32> for LogLevel { + fn into(self) -> i32 { + match self { + LogLevel::Error => 0, + LogLevel::Warning => 1, + LogLevel::Info => 2, + LogLevel::DebugL1 => 3, + LogLevel::Debug => 4, + LogLevel::DebugL2 => 5, + } + } +} |