nitrokey-0.3.4/.builds/archlinux-use-system-lib.yaml010064400017500001750000000006551341771277700207320ustar0000000000000000image: archlinux packages: - rust - libnitrokey environment: USE_SYSTEM_LIBNITROKEY: "1" sources: - https://git.sr.ht/~ireas/nitrokey-rs tasks: - build: | cd nitrokey-rs cargo build --release - test: | cd nitrokey-rs cargo test - format: | cd nitrokey-rs cargo fmt -- --check triggers: - action: email condition: failure to: nitrokey-rs-dev nitrokey-0.3.4/.builds/archlinux.yml010064400017500001750000000006051341771277700157040ustar0000000000000000image: archlinux packages: - rust - hidapi - gcc sources: - https://git.sr.ht/~ireas/nitrokey-rs tasks: - build: | cd nitrokey-rs cargo build --release - test: | cd nitrokey-rs cargo test - format: | cd nitrokey-rs cargo fmt -- --check triggers: - action: email condition: failure to: nitrokey-rs-dev nitrokey-0.3.4/.gitignore010064400017500001750000000000721341242041600135670ustar0000000000000000 /target /nitrokey-sys/target **/*.rs.bk Cargo.lock *.swp nitrokey-0.3.4/CHANGELOG.md010064400017500001750000000060221342107757500134270ustar0000000000000000# v0.3.4 (2019-01-20) - Fix authentication methods that assumed that `char` is signed. # v0.3.3 (2019-01-16) - Add the `get_production_info` and `clear_new_sd_card_warning` methods to the `Storage` struct. - Use `rand_os` instead of `rand` for random data creation. - (Re-)add `CommandError::RngError` variant. - Account for the possibility that an empty string returned by libnitrokey can not only indicate an error but also be a valid return value. - Make test cases more robust and avoid side effects on other test cases. # v0.3.2 (2019-01-12) - Make three additional error codes known: `CommandError::StringTooLong`, `CommandError::InvalidHexString` and `CommandError::TargetBufferTooSmall`. - Add the `get_library_version` function to query the libnitrokey version. - Add the `wink` method to the `Storage` struct. - Add the `set_unencrypted_volume_mode` to set the access mode of the unencrypted volume. - Add the `export_firmware` method to the `Storage` struct. # v0.3.1 (2019-01-07) - Use `nitrokey-test` to select and execute the unit tests. - Add support for the hidden volumes on a Nitrokey Storage (`enable_hidden_volume`, `disable_hidden_volume` and `create_hidden_volume` methods for the `Storage` struct). - Add the `connect_model` function to connect to a specific model using an enum variant. # v0.3.0 (2019-01-04) - Add a `force` argument to `ConfigureOtp::set_time`. - Remove the obsolete `CommandError::RngError`. - Add `CommandError::Undefined` to represent errors without further information (e. g. a method returned `NULL` unexpectedly). - Add error code to `CommandError::Unknown`. - Add the `Storage::change_update_pin` method that changes the firmware update PIN. - Add the `Device::factory_reset` method that performs a factory reset. - Add the `Device::build_aes_key` method that builds a new AES key on the Nitrokey. - Add the `Storage::enable_firmware_update` method that puts the Nitrokey Storage in update mode so that the firmware can be updated. # v0.2.3 (2018-12-31) - Dummy release to fix an issue with the crates.io tarball. # v0.2.2 (2018-12-30) - Update to Rust edition 2018. - Remove the `test-no-device` feature. - Update the rand dependency to version 0.6. - Add function `Device::get_model` that returns the connected model. - Derive the `Copy` and `Clone` traits for the enums `CommandError`, `LogLevel` and `OtpMode` # 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 nitrokey-0.3.4/Cargo.toml.orig010064400017500001750000000012311342107757500145020ustar0000000000000000[package] name = "nitrokey" version = "0.3.4" authors = ["Robin Krahl "] edition = "2018" 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-pro = [] test-storage = [] [dependencies] libc = "0.2" nitrokey-sys = "3.4" rand_core = {version = "0.3", default-features = false} rand_os = {version = "0.1"} [dev-dependencies] nitrokey-test = {version = "0.1"} nitrokey-0.3.4/Cargo.toml0000644000000023260000000000000107440ustar00# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g. crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "nitrokey" version = "0.3.4" authors = ["Robin Krahl "] description = "Bindings to libnitrokey for communication with Nitrokey devices" homepage = "https://code.ireas.org/nitrokey-rs/" documentation = "https://docs.rs/nitrokey" readme = "README.md" keywords = ["nitrokey", "otp"] categories = ["api-bindings"] license = "MIT" repository = "https://git.ireas.org/nitrokey-rs/" [dependencies.libc] version = "0.2" [dependencies.nitrokey-sys] version = "3.4" [dependencies.rand_core] version = "0.3" default-features = false [dependencies.rand_os] version = "0.1" [dev-dependencies.nitrokey-test] version = "0.1" [features] test-pro = [] test-storage = [] nitrokey-0.3.4/LICENSE010064400017500001750000000021161327751234200126160ustar0000000000000000The MIT License (MIT) Copyright (c) 2018 Robin Krahl 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. nitrokey-0.3.4/README.md010064400017500001750000000072521342107070400130660ustar0000000000000000# nitrokey-rs A libnitrokey wrapper for Rust providing access to Nitrokey devices. [Documentation][] ## Compatibility The required [`libnitrokey`][] version is built from source. The host system must provide `libhidapi-libusb0` (Linux) or `libhidapi` (non-Linux) in the default library search path. Depending on your system, you might also have to install the [Nitrokey udev rules][]. Currently, this crate provides access to the common features of the Nitrokey Pro and the Nitrokey Storage: general configuration, OTP generation and the password safe. Basic support for the secure storage on the Nitrokey Storage is available but still under development. ### Unsupported Functions The following functions provided by `libnitrokey` are deliberately not supported by `nitrokey-rs`: - `NK_get_device_model`. We know which model we connected to, so we can provide this information without calling `libnitrokey`. - `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`. - `NK_set_unencrypted_volume_rorw_pin_type_user`, `NK_set_unencrypted_read_only`, `NK_set_unencrypted_read_write`, `NK_set_encrypted_read_only` and `NK_set_encrypted_read_write`. These methods are only relevant for older firmware versions (pre-v0.51). As the Nitrokey Storage firmware can be updated easily, we do not support these outdated versions. ## Tests This crate has tests for different scenarios: Some tests require that no Nitrokey device is connected, others require a Nitrokey Storage or a Nitrokey Pro. We use the [`nitrokey-test`][] crate to select the test cases. You can just run `cargo test` to auto-detect connected Nitrokey devices and to run the appropriate tests. If you want to manually select the tests, set the `NITROKEY_TEST_GROUP` environment variable to `nodev` (no device connected), `pro` (Nitrokey Pro connected) or `storage` (Nitrokey Storage connected). Note that the tests assume that the device’s passwords are the factory defaults (admin PIN `12345678`, user PIN `123456`, update password `12345678`) and that an AES key has been built. Some tests will overwrite the data stored on the Nitrokey device or perform a factory reset. Never execute the tests if you unless yout want to destroy all data on all connected Nitrokey devices! 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. Thanks to Daniel Mueller for contributions to `nitrokey-rs` and for the `nitrokey-test` 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 [Nitrokey udev rules]: https://www.nitrokey.com/documentation/frequently-asked-questions-faq#openpgp-card-not-available [`libnitrokey`]: https://github.com/nitrokey/libnitrokey [`nitrokey-test`]: https://github.com/d-e-s-o/nitrokey-test [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 nitrokey-0.3.4/TODO.md010064400017500001750000000017071342107070400126750ustar0000000000000000- Add support for the currently unsupported commands: - `NK_is_AES_supported` - `NK_send_startup` - `NK_fill_SD_card_with_random_data` - `NK_get_SD_usage_data_as_string` - `NK_get_progress_bar_value` - `NK_list_devices_by_cpuID` - `NK_connect_with_ID` - 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. - Check integer conversions. - Consider implementing `Into` 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 nitrokey-0.3.4/src/auth.rs010064400017500001750000000341311342107627200137070ustar0000000000000000use std::ops::Deref; use std::os::raw::c_char; use std::os::raw::c_int; use nitrokey_sys; use crate::config::{Config, RawConfig}; use crate::device::{Device, DeviceWrapper, Pro, Storage}; use crate::otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData, RawOtpSlotData}; use crate::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) {} /// 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, (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) {} /// 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, (Self, CommandError)> where Self: Device + Sized; } trait AuthenticatedDevice { fn new(device: T, temp_password: Vec) -> 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 { device: T, temp_password: Vec, } /// 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 { device: T, temp_password: Vec, } fn authenticate(device: D, password: &str, callback: T) -> Result where D: Device, A: AuthenticatedDevice, T: Fn(*const c_char, *const c_char) -> c_int, { let temp_password = match generate_password(TEMPORARY_PASSWORD_LENGTH) { Ok(temp_password) => temp_password, Err(err) => return Err((device, err)), }; 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 c_char; 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( device: T, constructor: C, password: &str, ) -> Result, (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( device: T, constructor: C, password: &str, ) -> Result, (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 User { /// 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 Deref for User { type Target = T; fn deref(&self) -> &Self::Target { &self.device } } impl GenerateOtp for User { fn get_hotp_code(&self, slot: u8) -> Result { unsafe { let temp_password_ptr = self.temp_password.as_ptr() as *const c_char; return result_from_string(nitrokey_sys::NK_get_hotp_code_PIN(slot, temp_password_ptr)); } } fn get_totp_code(&self, slot: u8) -> Result { unsafe { let temp_password_ptr = self.temp_password.as_ptr() as *const c_char; return result_from_string(nitrokey_sys::NK_get_totp_code_PIN( slot, 0, 0, 0, temp_password_ptr, )); } } } impl AuthenticatedDevice for User { fn new(device: T, temp_password: Vec) -> Self { User { device, temp_password, } } } impl Deref for Admin { type Target = T; fn deref(&self) -> &Self::Target { &self.device } } impl Admin { /// 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 c_char, )) } } fn write_otp_slot(&self, data: OtpSlotData, callback: C) -> Result<(), CommandError> where C: Fn(RawOtpSlotData, *const c_char) -> c_int, { let raw_data = RawOtpSlotData::new(data)?; let temp_password_ptr = self.temp_password.as_ptr() as *const c_char; get_command_result(callback(raw_data, temp_password_ptr)) } } impl ConfigureOtp for Admin { 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 c_char; 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 c_char; unsafe { get_command_result(nitrokey_sys::NK_erase_totp_slot(slot, temp_password_ptr)) } } } impl AuthenticatedDevice for Admin { fn new(device: T, temp_password: Vec) -> Self { Admin { device, temp_password, } } } impl Authenticate for DeviceWrapper { fn authenticate_user(self, password: &str) -> Result, (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, (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, (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, (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, (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, (Self, CommandError)> { authenticate(self, password, |password_ptr, temp_password_ptr| unsafe { nitrokey_sys::NK_first_authenticate(password_ptr, temp_password_ptr) }) } } nitrokey-0.3.4/src/config.rs010064400017500001750000000055441342107070400142130ustar0000000000000000use crate::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, /// 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, /// 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, /// 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 { if value < 3 { return Some(value); } None } fn option_to_config_otp_slot(value: Option) -> Result { 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, capslock: Option, scrollock: Option, user_password: bool, ) -> Config { Config { numlock, capslock, scrollock, user_password, } } } impl RawConfig { pub fn try_from(config: Config) -> Result { 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 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, } } } nitrokey-0.3.4/src/device.rs010064400017500001750000001327541342107070400142110ustar0000000000000000use std::fmt; use libc; use nitrokey_sys; use crate::auth::Authenticate; use crate::config::{Config, RawConfig}; use crate::otp::GenerateOtp; use crate::pws::GetPasswordSafe; use crate::util::{ get_command_result, get_cstring, get_last_error, result_from_string, CommandError, }; /// Available Nitrokey models. #[derive(Clone, Copy, Debug, PartialEq)] pub enum Model { /// The Nitrokey Storage. Storage, /// The Nitrokey Pro. Pro, } impl fmt::Display for Model { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", match *self { Model::Pro => "Pro", Model::Storage => "Storage", } ) } } /// The access mode of a volume on the Nitrokey Storage. #[derive(Clone, Copy, Debug, PartialEq)] pub enum VolumeMode { /// A read-only volume. ReadOnly, /// A read-write volume. ReadWrite, } impl fmt::Display for VolumeMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { VolumeMode::ReadOnly => f.write_str("read-only"), VolumeMode::ReadWrite => f.write_str("read-write"), } } } /// 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) {} /// 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 #[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) {} /// 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) {} /// 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, } /// Information about the SD card in a Storage device. #[derive(Debug)] pub struct SdCardData { /// The serial number of the SD card. pub serial_number: u32, /// The size of the SD card in GB. pub size: u8, /// The year the card was manufactured, e. g. 17 for 2017. pub manufacturing_year: u8, /// The month the card was manufactured. pub manufacturing_month: u8, /// The OEM ID. pub oem: u16, /// The manufacturer ID. pub manufacturer: u8, } #[derive(Debug)] /// Production information for a Storage device. pub struct StorageProductionInfo { /// 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, /// The internal firmware version. pub firmware_version_internal: u8, /// The serial number of the CPU. pub serial_number_cpu: u32, /// Information about the SD card. pub sd_card: SdCardData, } /// 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 model of the connected Nitrokey device. /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::connect()?; /// println!("Connected to a Nitrokey {}", device.get_model()); /// # Ok(()) /// # } fn get_model(&self) -> Model; /// 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 { 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 { 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()) } } /// Performs a factory reset on the Nitrokey device. /// /// This commands performs a factory reset on the smart card (like the factory reset via `gpg /// --card-edit`) and then clears the flash memory (password safe, one-time passwords etc.). /// After a factory reset, [`build_aes_key`][] has to be called before the password safe or the /// encrypted volume can be used. /// /// # Errors /// /// - [`InvalidString`][] if the provided password contains a null byte /// - [`WrongPassword`][] if the admin password is wrong /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::connect()?; /// match device.factory_reset("12345678") { /// Ok(()) => println!("Performed a factory reset."), /// Err(err) => println!("Could not perform a factory reset: {}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`build_aes_key`]: #method.build_aes_key fn factory_reset(&self, admin_pin: &str) -> Result<(), CommandError> { let admin_pin_string = get_cstring(admin_pin)?; unsafe { get_command_result(nitrokey_sys::NK_factory_reset(admin_pin_string.as_ptr())) } } /// Builds a new AES key on the Nitrokey. /// /// The AES key is used to encrypt the password safe and the encrypted volume. You may need /// to call this method after a factory reset, either using [`factory_reset`][] or using `gpg /// --card-edit`. You can also use it to destroy the data stored in the password safe or on /// the encrypted volume. /// /// # Errors /// /// - [`InvalidString`][] if the provided password contains a null byte /// - [`WrongPassword`][] if the admin password is wrong /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::connect()?; /// match device.build_aes_key("12345678") { /// Ok(()) => println!("New AES keys have been built."), /// Err(err) => println!("Could not build new AES keys: {}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`factory_reset`]: #method.factory_reset fn build_aes_key(&self, admin_pin: &str) -> Result<(), CommandError> { let admin_pin_string = get_cstring(admin_pin)?; unsafe { get_command_result(nitrokey_sys::NK_build_aes_key(admin_pin_string.as_ptr())) } } } /// Connects to a Nitrokey device. This method can be used to connect to any connected device, /// both a Nitrokey Pro and a Nitrokey Storage. /// /// # Errors /// /// - [`Undefined`][] if no Nitrokey device is connected /// /// # 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), /// } /// ``` /// /// [`Undefined`]: enum.CommandError.html#variant.Undefined pub fn connect() -> Result { unsafe { match nitrokey_sys::NK_login_auto() { 1 => match get_connected_device() { Some(wrapper) => Ok(wrapper), None => Err(CommandError::Undefined), }, _ => Err(CommandError::Undefined), } } } /// Connects to a Nitrokey device of the given model. /// /// # Errors /// /// - [`Undefined`][] if no Nitrokey device of the given model is connected /// /// # Example /// /// ``` /// use nitrokey::DeviceWrapper; /// use nitrokey::Model; /// /// fn do_something(device: DeviceWrapper) {} /// /// match nitrokey::connect_model(Model::Pro) { /// Ok(device) => do_something(device), /// Err(err) => println!("Could not connect to a Nitrokey Pro: {}", err), /// } /// ``` /// /// [`Undefined`]: enum.CommandError.html#variant.Undefined pub fn connect_model(model: Model) -> Result { if connect_enum(model) { Ok(create_device_wrapper(model)) } else { Err(CommandError::Undefined) } } fn get_connected_model() -> Option { 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 { get_connected_model().map(create_device_wrapper) } fn connect_enum(model: Model) -> bool { let model = match model { Model::Storage => nitrokey_sys::NK_device_model_NK_STORAGE, Model::Pro => nitrokey_sys::NK_device_model_NK_PRO, }; unsafe { nitrokey_sys::NK_login_enum(model) == 1 } } impl DeviceWrapper { fn device(&self) -> &dyn 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 { self.device().get_hotp_slot_name(slot) } fn get_totp_slot_name(&self, slot: u8) -> Result { self.device().get_totp_slot_name(slot) } fn get_hotp_code(&self, slot: u8) -> Result { self.device().get_hotp_code(slot) } fn get_totp_code(&self, slot: u8) -> Result { self.device().get_totp_code(slot) } } impl Device for DeviceWrapper { fn get_model(&self) -> Model { match *self { DeviceWrapper::Pro(_) => Model::Pro, DeviceWrapper::Storage(_) => Model::Storage, } } } impl Pro { /// Connects to a Nitrokey Pro. /// /// # Errors /// /// - [`Undefined`][] if no Nitrokey device of the given model is connected /// /// # Example /// /// ``` /// use nitrokey::Pro; /// /// fn use_pro(device: Pro) {} /// /// match nitrokey::Pro::connect() { /// Ok(device) => use_pro(device), /// Err(err) => println!("Could not connect to the Nitrokey Pro: {}", err), /// } /// ``` /// /// [`Undefined`]: enum.CommandError.html#variant.Undefined pub fn connect() -> Result { // TODO: maybe Option instead of Result? match connect_enum(Model::Pro) { true => Ok(Pro {}), false => Err(CommandError::Undefined), } } } impl Drop for Pro { fn drop(&mut self) { unsafe { nitrokey_sys::NK_logout(); } } } impl Device for Pro { fn get_model(&self) -> Model { Model::Pro } } impl GenerateOtp for Pro {} impl Storage { /// Connects to a Nitrokey Storage. /// /// # Errors /// /// - [`Undefined`][] if no Nitrokey device of the given model is connected /// /// # Example /// /// ``` /// use nitrokey::Storage; /// /// fn use_storage(device: Storage) {} /// /// match nitrokey::Storage::connect() { /// Ok(device) => use_storage(device), /// Err(err) => println!("Could not connect to the Nitrokey Storage: {}", err), /// } /// ``` /// /// [`Undefined`]: enum.CommandError.html#variant.Undefined pub fn connect() -> Result { // TODO: maybe Option instead of Result? match connect_enum(Model::Storage) { true => Ok(Storage {}), false => Err(CommandError::Undefined), } } /// Changes the update PIN. /// /// The update PIN is used to enable firmware updates. Unlike the user and the admin PIN, the /// update PIN is not managed by the OpenPGP smart card but by the Nitrokey firmware. There is /// no retry counter as with the other PIN types. /// /// # Errors /// /// - [`InvalidString`][] if one of the provided passwords contains a null byte /// - [`WrongPassword`][] if the current update password is wrong /// /// # Example /// /// ```no_run /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::Storage::connect()?; /// match device.change_update_pin("12345678", "87654321") { /// Ok(()) => println!("Updated update PIN."), /// Err(err) => println!("Failed to update update PIN: {}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword pub fn change_update_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_update_password( current_string.as_ptr(), new_string.as_ptr(), )) } } /// Enables the firmware update mode. /// /// During firmware update mode, the Nitrokey can no longer be accessed using HID commands. /// To resume normal operation, run `dfu-programmer at32uc3a3256s launch`. In order to enter /// the firmware update mode, you need the update password that can be changed using the /// [`change_update_pin`][] method. /// /// # Errors /// /// - [`InvalidString`][] if one of the provided passwords contains a null byte /// - [`WrongPassword`][] if the current update password is wrong /// /// # Example /// /// ```no_run /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::Storage::connect()?; /// match device.enable_firmware_update("12345678") { /// Ok(()) => println!("Nitrokey entered update mode."), /// Err(err) => println!("Could not enter update mode: {}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword pub fn enable_firmware_update(&self, update_pin: &str) -> Result<(), CommandError> { let update_pin_string = get_cstring(update_pin)?; unsafe { get_command_result(nitrokey_sys::NK_enable_firmware_update( update_pin_string.as_ptr(), )) } } /// 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()) } } /// Enables a hidden storage volume. /// /// This function will only succeed if the encrypted storage ([`enable_encrypted_volume`][]) or /// another hidden volume has been enabled previously. Once the hidden volume is enabled, it /// is presented to the operating system as a block device and any previously opened encrypted /// or hidden volumes are closed. The API does not provide any information on the name or path /// of this block device. /// /// Note that the encrypted and the hidden volumes operate on the same storage area, so using /// both at the same time might lead to data loss. /// /// The hidden volume to unlock is selected based on the provided password. /// /// # Errors /// /// - [`AesDecryptionFailed`][] if the encrypted storage has not been opened before calling /// this method or the AES key has not been built /// - [`InvalidString`][] if the provided password contains a null byte /// /// # Example /// /// ```no_run /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::Storage::connect()?; /// device.enable_encrypted_volume("123445")?; /// match device.enable_hidden_volume("hidden-pw") { /// Ok(()) => println!("Enabled a hidden volume."), /// Err(err) => println!("Could not enable the hidden volume: {}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`enable_encrypted_volume`]: #method.enable_encrypted_volume /// [`AesDecryptionFailed`]: enum.CommandError.html#variant.AesDecryptionFailed /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString pub fn enable_hidden_volume(&self, volume_password: &str) -> Result<(), CommandError> { let volume_password = get_cstring(volume_password)?; unsafe { get_command_result(nitrokey_sys::NK_unlock_hidden_volume( volume_password.as_ptr(), )) } } /// Disables a hidden storage volume. /// /// Once the volume is disabled, it can be no longer accessed as a block device. If no hidden /// volume has 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()?; /// device.enable_encrypted_volume("123445")?; /// match device.enable_hidden_volume("hidden-pw") { /// Ok(()) => { /// println!("Enabled the hidden volume."); /// use_volume(); /// match device.disable_hidden_volume() { /// Ok(()) => println!("Disabled the hidden volume."), /// Err(err) => { /// println!("Could not disable the hidden volume: {}", err); /// }, /// }; /// }, /// Err(err) => println!("Could not enable the hidden volume: {}", err), /// }; /// # Ok(()) /// # } /// ``` pub fn disable_hidden_volume(&self) -> Result<(), CommandError> { unsafe { get_command_result(nitrokey_sys::NK_lock_hidden_volume()) } } /// Creates a hidden volume. /// /// The volume is crated in the given slot and in the given range of the available memory, /// where `start` is the start position as a percentage of the available memory, and `end` is /// the end position as a percentage of the available memory. The volume will be protected by /// the given password. /// /// Note that the encrypted and the hidden volumes operate on the same storage area, so using /// both at the same time might lead to data loss. /// /// According to the libnitrokey documentation, this function only works if the encrypted /// storage has been opened. /// /// # Errors /// /// - [`AesDecryptionFailed`][] if the encrypted storage has not been opened before calling /// this method or the AES key has not been built /// - [`InvalidString`][] if the provided password contains a null byte /// /// # Example /// /// ```no_run /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::Storage::connect()?; /// device.enable_encrypted_volume("123445")?; /// device.create_hidden_volume(0, 0, 100, "hidden-pw")?; /// # Ok(()) /// # } /// ``` /// /// [`AesDecryptionFailed`]: enum.CommandError.html#variant.AesDecryptionFailed /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString pub fn create_hidden_volume( &self, slot: u8, start: u8, end: u8, password: &str, ) -> Result<(), CommandError> { let password = get_cstring(password)?; unsafe { get_command_result(nitrokey_sys::NK_create_hidden_volume( slot, start, end, password.as_ptr(), )) } } /// Sets the access mode of the unencrypted volume. /// /// This command will reconnect the unencrypted volume so buffers should be flushed before /// calling it. Since firmware version v0.51, this command requires the admin PIN. Older /// firmware versions are not supported. /// /// # Errors /// /// - [`InvalidString`][] if the provided password contains a null byte /// - [`WrongPassword`][] if the provided admin password is wrong /// /// # Example /// /// ```no_run /// # use nitrokey::CommandError; /// use nitrokey::VolumeMode; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::Storage::connect()?; /// match device.set_unencrypted_volume_mode("123456", VolumeMode::ReadWrite) { /// Ok(()) => println!("Set the unencrypted volume to read-write mode."), /// Err(err) => println!("Could not set the unencrypted volume to read-write mode: {}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword pub fn set_unencrypted_volume_mode( &self, admin_pin: &str, mode: VolumeMode, ) -> Result<(), CommandError> { let admin_pin = get_cstring(admin_pin)?; let result = match mode { VolumeMode::ReadOnly => unsafe { nitrokey_sys::NK_set_unencrypted_read_only_admin(admin_pin.as_ptr()) }, VolumeMode::ReadWrite => unsafe { nitrokey_sys::NK_set_unencrypted_read_write_admin(admin_pin.as_ptr()) }, }; get_command_result(result) } /// 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 { 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))) } /// Returns the production information for 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_production_info() { /// Ok(data) => { /// println!("SD card ID: {:#x}", data.sd_card.serial_number); /// println!("SD card size: {} GB", data.sd_card.size); /// }, /// Err(err) => println!("Could not get Storage production info: {}", err), /// }; /// # Ok(()) /// # } /// ``` pub fn get_production_info(&self) -> Result { let mut raw_data = nitrokey_sys::NK_storage_ProductionTest { FirmwareVersion_au8: [0, 2], FirmwareVersionInternal_u8: 0, SD_Card_Size_u8: 0, CPU_CardID_u32: 0, SmartCardID_u32: 0, SD_CardID_u32: 0, SC_UserPwRetryCount: 0, SC_AdminPwRetryCount: 0, SD_Card_ManufacturingYear_u8: 0, SD_Card_ManufacturingMonth_u8: 0, SD_Card_OEM_u16: 0, SD_WriteSpeed_u16: 0, SD_Card_Manufacturer_u8: 0, }; let raw_result = unsafe { nitrokey_sys::NK_get_storage_production_info(&mut raw_data) }; let result = get_command_result(raw_result); result.and(Ok(StorageProductionInfo::from(raw_data))) } /// Clears the warning for a new SD card. /// /// The Storage status contains a field for a new SD card warning. After a factory reset, the /// field is set to true. After filling the SD card with random data, it is set to false. /// This method can be used to set it to false without filling the SD card with random data. /// /// # Errors /// /// - [`InvalidString`][] if the provided password contains a null byte /// - [`WrongPassword`][] if the provided admin password is wrong /// /// # Example /// /// ```no_run /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::Storage::connect()?; /// match device.clear_new_sd_card_warning("12345678") { /// Ok(()) => println!("Cleared the new SD card warning."), /// Err(err) => println!("Could not set the clear the new SD card warning: {}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword pub fn clear_new_sd_card_warning(&self, admin_pin: &str) -> Result<(), CommandError> { let admin_pin = get_cstring(admin_pin)?; get_command_result(unsafe { nitrokey_sys::NK_clear_new_sd_card_warning(admin_pin.as_ptr()) }) } /// Blinks the red and green LED alternatively and infinitely until the device is reconnected. pub fn wink(&self) -> Result<(), CommandError> { get_command_result(unsafe { nitrokey_sys::NK_wink() }) } /// Exports the firmware to the unencrypted volume. /// /// This command requires the admin PIN. The unencrypted volume must be in read-write mode /// when this command is executed. Otherwise, it will still return `Ok` but not write the /// firmware. /// /// This command unmounts the unencrypted volume if it has been mounted, so all buffers should /// be flushed. The firmware is written to the `firmware.bin` file on the unencrypted volume. /// /// # Errors /// /// - [`InvalidString`][] if one of the provided passwords contains a null byte /// - [`WrongPassword`][] if the admin password is wrong /// /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword pub fn export_firmware(&self, admin_pin: &str) -> Result<(), CommandError> { let admin_pin_string = get_cstring(admin_pin)?; get_command_result(unsafe { nitrokey_sys::NK_export_firmware(admin_pin_string.as_ptr()) }) } } impl Drop for Storage { fn drop(&mut self) { unsafe { nitrokey_sys::NK_logout(); } } } impl Device for Storage { fn get_model(&self) -> Model { Model::Storage } } impl GenerateOtp for Storage {} impl From for StorageProductionInfo { fn from(data: nitrokey_sys::NK_storage_ProductionTest) -> Self { Self { firmware_version_major: data.FirmwareVersion_au8[0], firmware_version_minor: data.FirmwareVersion_au8[1], firmware_version_internal: data.FirmwareVersionInternal_u8, serial_number_cpu: data.CPU_CardID_u32, sd_card: SdCardData { serial_number: data.SD_CardID_u32, size: data.SD_Card_Size_u8, manufacturing_year: data.SD_Card_ManufacturingYear_u8, manufacturing_month: data.SD_Card_ManufacturingMonth_u8, oem: data.SD_Card_OEM_u16, manufacturer: data.SD_Card_Manufacturer_u8, }, } } } impl From 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, } } } nitrokey-0.3.4/src/lib.rs010064400017500001750000000142221342107070400135050ustar0000000000000000//! 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 #![warn(missing_docs, rust_2018_compatibility, rust_2018_idioms, unused)] mod auth; mod config; mod device; mod otp; mod pws; mod util; use nitrokey_sys; pub use crate::auth::{Admin, Authenticate, User}; pub use crate::config::Config; pub use crate::device::{ connect, connect_model, Device, DeviceWrapper, Model, Pro, SdCardData, Storage, StorageProductionInfo, StorageStatus, VolumeMode, VolumeStatus, }; pub use crate::otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; pub use crate::pws::{GetPasswordSafe, PasswordSafe, SLOT_COUNT}; pub use crate::util::{CommandError, LogLevel}; /// A version of the libnitrokey library. /// /// Use the [`get_library_version`](fn.get_library_version.html) function to query the library /// version. #[derive(Clone, Debug, PartialEq)] pub struct Version { /// The Git library version as a string. /// /// The library version is the output of `git describe --always` at compile time, for example /// `v3.3` or `v3.4.1`. If the library has not been built from a release, the version string /// contains the number of commits since the last release and the hash of the current commit, for /// example `v3.3-19-gaee920b`. If the library has not been built from a Git checkout, this /// string may be empty. pub git: String, /// The major library version. pub major: u32, /// The minor library version. pub minor: u32, } /// 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()); } } /// Returns the libnitrokey library version. /// /// # Example /// /// ``` /// let version = nitrokey::get_library_version(); /// println!("Using libnitrokey {}", version.git); /// ``` pub fn get_library_version() -> Version { // NK_get_library_version returns a static string, so we don’t have to free the pointer. let git = unsafe { nitrokey_sys::NK_get_library_version() }; let git = if git.is_null() { String::new() } else { util::owned_str_from_ptr(git) }; let major = unsafe { nitrokey_sys::NK_get_major_library_version() }; let minor = unsafe { nitrokey_sys::NK_get_minor_library_version() }; Version { git, major, minor } } nitrokey-0.3.4/src/otp.rs010064400017500001750000000343701342107070400135470ustar0000000000000000use std::ffi::CString; use nitrokey_sys; use crate::util::{get_command_result, get_cstring, result_from_string, CommandError}; /// Modes for one-time password generation. #[derive(Clone, Copy, 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. /// /// `time` is the number of seconds since January 1st, 1970 (Unix timestamp). Unless `force` /// is set to `true`, this command fails if the timestamp on the device is larger than the /// given timestamp or if it is zero. /// /// The time is used for TOTP generation (see [`get_totp_code`][]). /// /// # Example /// /// ```no_run /// use std::time; /// use nitrokey::GenerateOtp; /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::connect()?; /// let time = time::SystemTime::now().duration_since(time::UNIX_EPOCH); /// match time { /// Ok(time) => device.set_time(time.as_secs(), false)?, /// Err(_) => println!("The system time is before the Unix epoch!"), /// } /// # 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, force: bool) -> Result<(), CommandError> { let result = if force { unsafe { nitrokey_sys::NK_totp_set_time(time) } } else { unsafe { nitrokey_sys::NK_totp_set_time_soft(time) } }; get_command_result(result) } /// 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 { 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 { 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 { 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 /// /// ```no_run /// use std::time; /// use nitrokey::GenerateOtp; /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::connect()?; /// let time = time::SystemTime::now().duration_since(time::UNIX_EPOCH); /// match time { /// Ok(time) => { /// device.set_time(time.as_secs(), false)?; /// let code = device.get_totp_code(1)?; /// println!("Generated TOTP code on slot 1: {}", code); /// }, /// Err(_) => println!("Timestamps before 1970-01-01 are not supported!"), /// } /// # 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 { 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, } #[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, T: Into>( 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>(mut self, id: S) -> OtpSlotData { self.token_id = Some(id.into()); self } } impl RawOtpSlotData { pub fn new(data: OtpSlotData) -> Result { 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, }) } } nitrokey-0.3.4/src/pws.rs010064400017500001750000000330211342107070400135460ustar0000000000000000use libc; use nitrokey_sys; use crate::device::{Device, DeviceWrapper, Pro, Storage}; use crate::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 dyn 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. /// /// If this method returns an `AesDecryptionFailed` (Nitrokey Pro) or `Unknown` (Nitrokey /// Storage) error, the AES data object on the smart card could not be accessed. This problem /// occurs after a factory reset using `gpg --card-edit` and can be fixed using the /// [`Device::build_aes_key`][] command. /// /// # Errors /// /// - [`AesDecryptionFailed`][] if the secret for the password safe could not be decrypted /// (Nitrokey Pro only) /// - [`InvalidString`][] if one of the provided passwords contains a null byte /// - [`Unknown`][] if the secret for the password safe could not be decrypted (Nitrokey /// Storage only) /// - [`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 /// [`AesDecryptionFailed`]: enum.CommandError.html#variant.AesDecryptionFailed /// [`Device::build_aes_key`]: trait.Device.html#method.build_aes_key /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString /// [`Unknown`]: enum.CommandError.html#variant.Unknown /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword fn get_password_safe(&self, user_pin: &str) -> Result, CommandError>; } fn get_password_safe<'a>( device: &'a dyn Device, user_pin: &str, ) -> Result, 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 }) } fn get_pws_result(s: String) -> Result { if s.is_empty() { Err(CommandError::SlotNotProgrammed) } else { Ok(s) } } 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). /// /// This method also returns a `SlotNotProgrammed` error if the name is empty. /// /// # Errors /// /// - [`InvalidSlot`][] if the given slot is out of range /// - [`SlotNotProgrammed`][] 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 /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed pub fn get_slot_name(&self, slot: u8) -> Result { unsafe { result_from_string(nitrokey_sys::NK_get_password_safe_slot_name(slot)) } .and_then(get_pws_result) } /// Returns the login for the given slot (if it is programmed). /// /// This method also returns a `SlotNotProgrammed` error if the login is empty. /// /// # Errors /// /// - [`InvalidSlot`][] if the given slot is out of range /// - [`SlotNotProgrammed`][] 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 /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed pub fn get_slot_login(&self, slot: u8) -> Result { unsafe { result_from_string(nitrokey_sys::NK_get_password_safe_slot_login(slot)) } .and_then(get_pws_result) } /// Returns the password for the given slot (if it is programmed). /// /// This method also returns a `SlotNotProgrammed` error if the password is empty. /// /// # Errors /// /// - [`InvalidSlot`][] if the given slot is out of range /// - [`SlotNotProgrammed`][] 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 /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed pub fn get_slot_password(&self, slot: u8) -> Result { unsafe { result_from_string(nitrokey_sys::NK_get_password_safe_slot_password(slot)) } .and_then(get_pws_result) } /// 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, CommandError> { get_password_safe(self, user_pin) } } impl GetPasswordSafe for Storage { fn get_password_safe(&self, user_pin: &str) -> Result, CommandError> { get_password_safe(self, user_pin) } } impl GetPasswordSafe for DeviceWrapper { fn get_password_safe(&self, user_pin: &str) -> Result, CommandError> { get_password_safe(self, user_pin) } } nitrokey-0.3.4/src/util.rs010064400017500001750000000162041342107070400137160ustar0000000000000000use std::borrow; use std::ffi::{CStr, CString}; use std::fmt; use std::os::raw::{c_char, c_int}; use libc::{c_void, free}; use rand_core::RngCore; use rand_os::OsRng; /// Error types returned by Nitrokey device or by the library. #[derive(Clone, Copy, 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 occurred 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 occurred. Unknown(i64), /// An unspecified error occurred. Undefined, /// You passed a string containing a null byte. InvalidString, /// A supplied string exceeded a length limit. StringTooLong, /// You passed an invalid slot. InvalidSlot, /// The supplied string was not in hexadecimal format. InvalidHexString, /// The target buffer was smaller than the source. TargetBufferTooSmall, /// An error occurred 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(Clone, Copy, 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 { if ptr.is_null() { return Err(CommandError::Undefined); } unsafe { let s = owned_str_from_ptr(ptr); free(ptr as *mut c_void); // An empty string can both indicate an error or be a valid return value. In this case, we // have to check the last command status to decide what to return. if s.is_empty() { get_last_result().map(|_| s) } else { 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::Undefined, Err(err) => err, }; } pub fn generate_password(length: usize) -> Result, CommandError> { let mut rng = OsRng::new()?; let mut data = vec![0u8; length]; rng.fill_bytes(&mut data[..]); Ok(data) } pub fn get_cstring>>(s: T) -> Result { CString::new(s).or(Err(CommandError::InvalidString)) } impl CommandError { fn as_str(&self) -> borrow::Cow<'static, str> { match *self { CommandError::WrongCrc => { "A packet with a wrong checksum has been sent or received".into() } CommandError::WrongSlot => "The given OTP slot does not exist".into(), CommandError::SlotNotProgrammed => "The given OTP slot is not programmed".into(), CommandError::WrongPassword => "The given password is wrong".into(), CommandError::NotAuthorized => { "You are not authorized for this command or provided a wrong temporary \ password" .into() } CommandError::Timestamp => "An error occurred when getting or setting the time".into(), CommandError::NoName => "You did not provide a name for the OTP slot".into(), CommandError::NotSupported => "This command is not supported by this device".into(), CommandError::UnknownCommand => "This command is unknown".into(), CommandError::AesDecryptionFailed => "AES decryption failed".into(), CommandError::Unknown(x) => { borrow::Cow::from(format!("An unknown error occurred ({})", x)) } CommandError::Undefined => "An unspecified error occurred".into(), CommandError::InvalidString => "You passed a string containing a null byte".into(), CommandError::StringTooLong => "The supplied string is too long".into(), CommandError::InvalidSlot => "The given slot is invalid".into(), CommandError::InvalidHexString => { "The supplied string is not in hexadecimal format".into() } CommandError::TargetBufferTooSmall => "The target buffer is too small".into(), CommandError::RngError => "An error occurred during random number generation".into(), } } } impl fmt::Display for CommandError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.as_str()) } } impl From 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, 200 => CommandError::StringTooLong, 201 => CommandError::InvalidSlot, 202 => CommandError::InvalidHexString, 203 => CommandError::TargetBufferTooSmall, x => CommandError::Unknown(x.into()), } } } impl From for CommandError { fn from(_error: rand_core::Error) -> Self { CommandError::RngError } } impl Into 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, } } } nitrokey-0.3.4/tests/device.rs010064400017500001750000000404531342107070400145560ustar0000000000000000mod util; use std::ffi::CStr; use std::process::Command; use std::{thread, time}; use nitrokey::{ Authenticate, CommandError, Config, ConfigureOtp, Device, GenerateOtp, GetPasswordSafe, OtpMode, OtpSlotData, Storage, VolumeMode, }; use nitrokey_test::test as test_device; use crate::util::{ADMIN_PASSWORD, USER_PASSWORD}; static ADMIN_NEW_PASSWORD: &str = "1234567890"; static UPDATE_PIN: &str = "12345678"; static UPDATE_NEW_PIN: &str = "87654321"; 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.replace("_", " ") == "Nitrokey Storage") .count() } #[test_device] fn connect_no_device() { assert!(nitrokey::connect().is_err()); assert!(nitrokey::connect_model(nitrokey::Model::Pro).is_err()); assert!(nitrokey::connect_model(nitrokey::Model::Storage).is_err()); assert!(nitrokey::Pro::connect().is_err()); assert!(nitrokey::Storage::connect().is_err()); } #[test_device] fn connect_pro(device: Pro) { assert_eq!(device.get_model(), nitrokey::Model::Pro); drop(device); assert!(nitrokey::connect().is_ok()); assert!(nitrokey::connect_model(nitrokey::Model::Pro).is_ok()); assert!(nitrokey::Pro::connect().is_ok()); } #[test_device] fn connect_storage(device: Storage) { assert_eq!(device.get_model(), nitrokey::Model::Storage); drop(device); assert!(nitrokey::connect().is_ok()); assert!(nitrokey::connect_model(nitrokey::Model::Storage).is_ok()); assert!(nitrokey::Storage::connect().is_ok()); } 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_device] fn disconnect(device: DeviceWrapper) { drop(device); assert_empty_serial_number(); } #[test_device] fn get_serial_number(device: DeviceWrapper) { 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_device] fn get_firmware_version(device: Pro) { assert_eq!(0, device.get_major_firmware_version()); let minor = device.get_minor_firmware_version(); assert!(minor > 0); } fn admin_retry(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(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_device] fn get_retry_count(device: DeviceWrapper) { 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_device] fn config(device: DeviceWrapper) { let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap(); let config = Config::new(None, None, None, true); assert_eq!(Ok(()), admin.write_config(config)); 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_eq!(Ok(()), admin.write_config(config)); let get_config = admin.get_config().unwrap(); assert_eq!(config, get_config); let config = Config::new(None, None, None, false); assert_eq!(Ok(()), admin.write_config(config)); let get_config = admin.get_config().unwrap(); assert_eq!(config, get_config); } #[test_device] fn change_user_pin(device: DeviceWrapper) { 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_device] fn change_admin_pin(device: DeviceWrapper) { 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: D, password: &str, error: CommandError) -> D where D: Device + Authenticate, nitrokey::User: std::fmt::Debug, { let result = device.authenticate_user(password); assert!(result.is_err()); let err = result.unwrap_err(); assert_eq!(error, err.1); err.0 } #[test_device] fn unlock_user_pin(device: DeviceWrapper) { 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) ); // block user PIN 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); // unblock with current PIN assert_eq!( Err(CommandError::WrongPassword), device.unlock_user_pin(USER_PASSWORD, USER_PASSWORD) ); assert!(device .unlock_user_pin(ADMIN_PASSWORD, USER_PASSWORD) .is_ok()); let device = device.authenticate_user(USER_PASSWORD).unwrap().device(); // block user PIN 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); // unblock with new PIN assert_eq!( Err(CommandError::WrongPassword), device.unlock_user_pin(USER_PASSWORD, USER_PASSWORD) ); assert!(device .unlock_user_pin(ADMIN_PASSWORD, USER_NEW_PASSWORD) .is_ok()); // reset user PIN assert!(device .change_user_pin(USER_NEW_PASSWORD, USER_PASSWORD) .is_ok()); } #[test_device] fn factory_reset(device: DeviceWrapper) { let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap(); let otp_data = OtpSlotData::new(1, "test", "0123468790", OtpMode::SixDigits); assert_eq!(Ok(()), admin.write_totp_slot(otp_data, 30)); let device = admin.device(); let pws = device.get_password_safe(USER_PASSWORD).unwrap(); assert_eq!(Ok(()), pws.write_slot(0, "test", "testlogin", "testpw")); drop(pws); assert_eq!( Ok(()), device.change_user_pin(USER_PASSWORD, USER_NEW_PASSWORD) ); assert_eq!( Ok(()), device.change_admin_pin(ADMIN_PASSWORD, ADMIN_NEW_PASSWORD) ); assert_eq!( Err(CommandError::WrongPassword), device.factory_reset(USER_NEW_PASSWORD) ); assert_eq!( Err(CommandError::WrongPassword), device.factory_reset(ADMIN_PASSWORD) ); assert_eq!(Ok(()), device.factory_reset(ADMIN_NEW_PASSWORD)); let device = device.authenticate_admin(ADMIN_PASSWORD).unwrap().device(); let user = device.authenticate_user(USER_PASSWORD).unwrap(); assert_eq!( Err(CommandError::SlotNotProgrammed), user.get_totp_slot_name(1) ); let device = user.device(); let pws = device.get_password_safe(USER_PASSWORD).unwrap(); assert_ne!("test".to_string(), pws.get_slot_name(0).unwrap()); assert_ne!("testlogin".to_string(), pws.get_slot_login(0).unwrap()); assert_ne!("testpw".to_string(), pws.get_slot_password(0).unwrap()); assert_eq!(Ok(()), device.build_aes_key(ADMIN_PASSWORD)); } #[test_device] fn build_aes_key(device: DeviceWrapper) { let pws = device.get_password_safe(USER_PASSWORD).unwrap(); assert_eq!(Ok(()), pws.write_slot(0, "test", "testlogin", "testpw")); drop(pws); assert_eq!( Err(CommandError::WrongPassword), device.build_aes_key(USER_PASSWORD) ); assert_eq!(Ok(()), device.build_aes_key(ADMIN_PASSWORD)); let device = device.authenticate_admin(ADMIN_PASSWORD).unwrap().device(); let pws = device.get_password_safe(USER_PASSWORD).unwrap(); assert_ne!("test".to_string(), pws.get_slot_name(0).unwrap()); assert_ne!("testlogin".to_string(), pws.get_slot_login(0).unwrap()); assert_ne!("testpw".to_string(), pws.get_slot_password(0).unwrap()); } #[test_device] fn change_update_pin(device: Storage) { assert_eq!( Err(CommandError::WrongPassword), device.change_update_pin(UPDATE_NEW_PIN, UPDATE_PIN) ); assert_eq!(Ok(()), device.change_update_pin(UPDATE_PIN, UPDATE_NEW_PIN)); assert_eq!(Ok(()), device.change_update_pin(UPDATE_NEW_PIN, UPDATE_PIN)); } #[test_device] fn encrypted_volume(device: Storage) { assert_eq!(Ok(()), device.lock()); assert_eq!(1, count_nitrokey_block_devices()); assert_eq!(Ok(()), device.disable_encrypted_volume()); 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_eq!(Ok(()), device.enable_encrypted_volume(USER_PASSWORD)); assert_eq!(2, count_nitrokey_block_devices()); assert_eq!(Ok(()), device.disable_encrypted_volume()); assert_eq!(1, count_nitrokey_block_devices()); } #[test_device] fn hidden_volume(device: Storage) { assert_eq!(Ok(()), device.lock()); assert_eq!(1, count_nitrokey_block_devices()); assert_eq!(Ok(()), device.disable_hidden_volume()); assert_eq!(1, count_nitrokey_block_devices()); assert_eq!(Ok(()), device.enable_encrypted_volume(USER_PASSWORD)); assert_eq!(2, count_nitrokey_block_devices()); // TODO: why this error code? assert_eq!( Err(CommandError::WrongPassword), device.create_hidden_volume(5, 0, 100, "hiddenpw") ); assert_eq!(Ok(()), device.create_hidden_volume(0, 20, 21, "hidden-pw")); assert_eq!( Ok(()), device.create_hidden_volume(0, 20, 21, "hiddenpassword") ); assert_eq!(Ok(()), device.create_hidden_volume(1, 0, 1, "otherpw")); // TODO: test invalid range (not handled by libnitrokey) assert_eq!(2, count_nitrokey_block_devices()); assert_eq!( Err(CommandError::WrongPassword), device.enable_hidden_volume("blubb") ); assert_eq!(Ok(()), device.enable_hidden_volume("hiddenpassword")); assert_eq!(2, count_nitrokey_block_devices()); assert_eq!(Ok(()), device.enable_hidden_volume("otherpw")); assert_eq!(2, count_nitrokey_block_devices()); assert_eq!(Ok(()), device.disable_hidden_volume()); assert_eq!(1, count_nitrokey_block_devices()); } #[test_device] fn lock(device: Storage) { assert_eq!(Ok(()), device.enable_encrypted_volume(USER_PASSWORD)); assert_eq!(Ok(()), device.lock()); assert_eq!(1, count_nitrokey_block_devices()); } #[test_device] fn set_unencrypted_volume_mode(device: Storage) { fn assert_mode(device: &Storage, mode: VolumeMode) { let status = device.get_status(); assert!(status.is_ok()); assert_eq!( status.unwrap().unencrypted_volume.read_only, mode == VolumeMode::ReadOnly ); } fn assert_success(device: &Storage, mode: VolumeMode) { assert_eq!( Ok(()), device.set_unencrypted_volume_mode(ADMIN_PASSWORD, mode) ); assert_mode(&device, mode); } assert_success(&device, VolumeMode::ReadOnly); assert_eq!( Err(CommandError::WrongPassword), device.set_unencrypted_volume_mode(USER_PASSWORD, VolumeMode::ReadOnly) ); assert_mode(&device, VolumeMode::ReadOnly); assert_success(&device, VolumeMode::ReadWrite); assert_success(&device, VolumeMode::ReadWrite); assert_success(&device, VolumeMode::ReadOnly); } #[test_device] fn get_storage_status(device: Storage) { let status = device.get_status().unwrap(); assert!(status.serial_number_sd_card > 0); assert!(status.serial_number_smart_card > 0); } #[test_device] fn get_production_info(device: Storage) { let info = device.get_production_info().unwrap(); assert_eq!(0, info.firmware_version_major); assert!(info.firmware_version_minor != 0); assert!(info.serial_number_cpu != 0); assert!(info.sd_card.serial_number != 0); assert!(info.sd_card.size > 0); assert!(info.sd_card.manufacturing_year > 10); assert!(info.sd_card.manufacturing_year < 100); // TODO: month value is not valid atm // assert!(info.sd_card.manufacturing_month < 12); assert!(info.sd_card.oem != 0); assert!(info.sd_card.manufacturer != 0); let status = device.get_status().unwrap(); assert_eq!(status.firmware_version_major, info.firmware_version_major); assert_eq!(status.firmware_version_minor, info.firmware_version_minor); assert_eq!(status.serial_number_sd_card, info.sd_card.serial_number); } #[test_device] fn clear_new_sd_card_warning(device: Storage) { assert_eq!(Ok(()), device.factory_reset(ADMIN_PASSWORD)); thread::sleep(time::Duration::from_secs(3)); assert_eq!(Ok(()), device.build_aes_key(ADMIN_PASSWORD)); // We have to perform an SD card operation to reset the new_sd_card_found field assert_eq!(Ok(()), device.lock()); let status = device.get_status().unwrap(); assert!(status.new_sd_card_found); assert_eq!(Ok(()), device.clear_new_sd_card_warning(ADMIN_PASSWORD)); let status = device.get_status().unwrap(); assert!(!status.new_sd_card_found); } #[test_device] fn export_firmware(device: Storage) { assert_eq!( Err(CommandError::WrongPassword), device.export_firmware("someadminpn") ); assert_eq!(Ok(()), device.export_firmware(ADMIN_PASSWORD)); assert_eq!( Ok(()), device.set_unencrypted_volume_mode(ADMIN_PASSWORD, VolumeMode::ReadWrite) ); assert_eq!(Ok(()), device.export_firmware(ADMIN_PASSWORD)); assert_eq!( Ok(()), device.set_unencrypted_volume_mode(ADMIN_PASSWORD, VolumeMode::ReadOnly) ); } nitrokey-0.3.4/tests/lib.rs010064400017500001750000000002761341771277700141060ustar0000000000000000#[test] fn get_library_version() { let version = nitrokey::get_library_version(); assert!(version.git.is_empty() || version.git.starts_with("v")); assert!(version.major > 0); } nitrokey-0.3.4/tests/otp.rs010064400017500001750000000237751342107541700141370ustar0000000000000000mod util; use std::fmt::Debug; use std::ops::Deref; use nitrokey::{ Admin, Authenticate, CommandError, Config, ConfigureOtp, Device, GenerateOtp, OtpMode, OtpSlotData, }; use nitrokey_test::test as test_device; use crate::util::{ADMIN_PASSWORD, USER_PASSWORD}; // 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 make_admin_test_device(device: T) -> Admin where T: Device, (T, nitrokey::CommandError): Debug, { device .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_eq!(Ok(()), admin.write_hotp_slot(slot_data, counter.into())); } 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_device] fn set_time(device: DeviceWrapper) { assert_eq!(Ok(()), device.set_time(1546385382, true)); assert_eq!(Ok(()), device.set_time(1546385392, false)); assert_eq!( Err(CommandError::Timestamp), device.set_time(1546385292, false) ); assert_eq!(Ok(()), device.set_time(1546385382, true)); } #[test_device] fn hotp_no_pin(device: DeviceWrapper) { let admin = make_admin_test_device(device); let config = Config::new(None, None, None, false); assert_eq!(Ok(()), admin.write_config(config)); 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_device] fn hotp_pin(device: DeviceWrapper) { let admin = make_admin_test_device(device); let config = Config::new(None, None, None, true); assert_eq!(Ok(()), admin.write_config(config)); 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_device] fn hotp_slot_name(device: DeviceWrapper) { let admin = make_admin_test_device(device); let slot_data = OtpSlotData::new(1, "test-hotp", HOTP_SECRET, OtpMode::SixDigits); assert_eq!(Ok(()), admin.write_hotp_slot(slot_data, 0)); 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_device] fn hotp_error(device: DeviceWrapper) { let admin = make_admin_test_device(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 slot_data = OtpSlotData::new(1, "test", "foobar", OtpMode::SixDigits); assert_eq!( Err(CommandError::InvalidHexString), admin.write_hotp_slot(slot_data, 0) ); let code = admin.get_hotp_code(4); assert_eq!(CommandError::InvalidSlot, code.unwrap_err()); } #[test_device] fn hotp_erase(device: DeviceWrapper) { let admin = make_admin_test_device(device); let config = Config::new(None, None, None, false); assert_eq!(Ok(()), admin.write_config(config)); let slot_data = OtpSlotData::new(1, "test1", HOTP_SECRET, OtpMode::SixDigits); assert_eq!(Ok(()), admin.write_hotp_slot(slot_data, 0)); let slot_data = OtpSlotData::new(2, "test2", HOTP_SECRET, OtpMode::SixDigits); assert_eq!(Ok(()), admin.write_hotp_slot(slot_data, 0)); assert_eq!(Ok(()), admin.erase_hotp_slot(1)); 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_eq!(Ok(()), admin.write_totp_slot(slot_data, time_window as u16)); } 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_eq!(Ok(()), device.set_time(time, true)); 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_device] fn totp_no_pin(device: DeviceWrapper) { // TODO: this test may fail due to bad timing --> find solution let admin = make_admin_test_device(device); let config = Config::new(None, None, None, false); assert_eq!(Ok(()), admin.write_config(config)); 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_device] // Nitrokey Storage does only support timestamps that fit in a 32-bit // unsigned integer, so don't test with it. fn totp_no_pin_64(device: Pro) { let admin = make_admin_test_device(device); let config = Config::new(None, None, None, false); assert_eq!(Ok(()), admin.write_config(config)); 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_device] fn totp_pin(device: DeviceWrapper) { // TODO: this test may fail due to bad timing --> find solution let admin = make_admin_test_device(device); let config = Config::new(None, None, None, true); assert_eq!(Ok(()), admin.write_config(config)); 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_device] // See comment for totp_no_pin_64. fn totp_pin_64(device: Pro) { let admin = make_admin_test_device(device); let config = Config::new(None, None, None, true); assert_eq!(Ok(()), admin.write_config(config)); 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_device] fn totp_slot_name(device: DeviceWrapper) { let admin = make_admin_test_device(device); let slot_data = OtpSlotData::new(1, "test-totp", TOTP_SECRET, OtpMode::EightDigits); assert_eq!(Ok(()), admin.write_totp_slot(slot_data, 0)); 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_device] fn totp_error(device: DeviceWrapper) { let admin = make_admin_test_device(device); let slot_data = OtpSlotData::new(1, "", TOTP_SECRET, OtpMode::SixDigits); assert_eq!( Err(CommandError::NoName), admin.write_totp_slot(slot_data, 0) ); let slot_data = OtpSlotData::new(20, "test", TOTP_SECRET, OtpMode::SixDigits); assert_eq!( Err(CommandError::InvalidSlot), admin.write_totp_slot(slot_data, 0) ); let slot_data = OtpSlotData::new(4, "test", "foobar", OtpMode::SixDigits); assert_eq!( Err(CommandError::InvalidHexString), admin.write_totp_slot(slot_data, 0) ); let code = admin.get_totp_code(20); assert_eq!(CommandError::InvalidSlot, code.unwrap_err()); } #[test_device] fn totp_erase(device: DeviceWrapper) { let admin = make_admin_test_device(device); let config = Config::new(None, None, None, false); assert_eq!(Ok(()), admin.write_config(config)); let slot_data = OtpSlotData::new(1, "test1", TOTP_SECRET, OtpMode::SixDigits); assert_eq!(Ok(()), admin.write_totp_slot(slot_data, 0)); let slot_data = OtpSlotData::new(2, "test2", TOTP_SECRET, OtpMode::SixDigits); assert_eq!(Ok(()), admin.write_totp_slot(slot_data, 0)); assert_eq!(Ok(()), admin.erase_totp_slot(1)); 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()); } nitrokey-0.3.4/tests/pws.rs010064400017500001750000000124161342107070400141260ustar0000000000000000mod util; use std::ffi::CStr; use libc::{c_int, c_void, free}; use nitrokey::{CommandError, Device, GetPasswordSafe, PasswordSafe, SLOT_COUNT}; use nitrokey_sys; use nitrokey_test::test as test_device; use crate::util::{ADMIN_PASSWORD, USER_PASSWORD}; fn get_slot_name_direct(slot: u8) -> Result { let ptr = unsafe { nitrokey_sys::NK_get_password_safe_slot_name(slot) }; if ptr.is_null() { return Err(CommandError::Undefined); } let s = unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() }; unsafe { free(ptr as *mut c_void) }; match s.is_empty() { true => { let error = unsafe { nitrokey_sys::NK_get_last_command_status() } as c_int; match error { 0 => Ok(s), other => Err(CommandError::from(other)), } } false => Ok(s), } } fn get_pws(device: &T) -> PasswordSafe where T: Device, { device.get_password_safe(USER_PASSWORD).unwrap() } #[test_device] fn enable(device: DeviceWrapper) { 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_device] fn drop(device: DeviceWrapper) { { let pws = get_pws(&device); assert_eq!(Ok(()), pws.write_slot(1, "name", "login", "password")); assert_eq!("name", pws.get_slot_name(1).unwrap()); let result = get_slot_name_direct(1); assert_eq!(Ok(String::from("name")), result); } let result = get_slot_name_direct(1); assert_eq!(Ok(String::from("name")), result); assert_eq!(Ok(()), device.lock()); let result = get_slot_name_direct(1); assert_eq!(Err(CommandError::NotAuthorized), result); } #[test_device] fn get_status(device: DeviceWrapper) { let pws = get_pws(&device); for i in 0..SLOT_COUNT { assert_eq!(Ok(()), pws.erase_slot(i), "Could not erase slot {}", i); } let status = pws.get_slot_status().unwrap(); assert_eq!(status, [false; SLOT_COUNT as usize]); assert_eq!(Ok(()), pws.write_slot(1, "name", "login", "password")); 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_eq!(Ok(()), pws.write_slot(i, "name", "login", "password")); } let status = pws.get_slot_status().unwrap(); assert_eq!(status, [true; SLOT_COUNT as usize]); } #[test_device] fn get_data(device: DeviceWrapper) { let pws = get_pws(&device); assert_eq!(Ok(()), pws.write_slot(1, "name", "login", "password")); 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!(Ok(()), pws.erase_slot(1)); assert_eq!(Err(CommandError::SlotNotProgrammed), pws.get_slot_name(1)); assert_eq!(Err(CommandError::SlotNotProgrammed), pws.get_slot_login(1)); assert_eq!( Err(CommandError::SlotNotProgrammed), pws.get_slot_password(1) ); let name = "with å"; let login = "pär@test.com"; let password = "'i3lJc[09?I:,[u7dWz9"; assert_eq!(Ok(()), pws.write_slot(1, name, login, password)); 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_device] fn write(device: DeviceWrapper) { let pws = get_pws(&device); assert_eq!( Err(CommandError::InvalidSlot), pws.write_slot(SLOT_COUNT, "name", "login", "password") ); assert_eq!(Ok(()), pws.write_slot(0, "", "login", "password")); assert_eq!(Err(CommandError::SlotNotProgrammed), 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_eq!(Ok(()), pws.write_slot(0, "name", "", "password")); assert_eq!(Ok(String::from("name")), pws.get_slot_name(0)); assert_eq!(Err(CommandError::SlotNotProgrammed), pws.get_slot_login(0)); assert_eq!(Ok(String::from("password")), pws.get_slot_password(0)); assert_eq!(Ok(()), pws.write_slot(0, "name", "login", "")); 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::SlotNotProgrammed), pws.get_slot_password(0) ); } #[test_device] fn erase(device: DeviceWrapper) { let pws = get_pws(&device); assert_eq!(Err(CommandError::InvalidSlot), pws.erase_slot(SLOT_COUNT)); assert_eq!(Ok(()), pws.write_slot(0, "name", "login", "password")); assert_eq!(Ok(()), pws.erase_slot(0)); assert_eq!(Ok(()), pws.erase_slot(0)); assert_eq!(Err(CommandError::SlotNotProgrammed), pws.get_slot_name(0)); } nitrokey-0.3.4/tests/util/mod.rs010064400017500001750000000001311342107070400150400ustar0000000000000000pub static ADMIN_PASSWORD: &str = "12345678"; pub static USER_PASSWORD: &str = "123456"; nitrokey-0.3.4/.cargo_vcs_info.json0000644000000001120000000000000127350ustar00{ "git": { "sha1": "b6a941cd9ed30a4f93299132ae7e155922c73701" } }