From 588066f415e956fdcd2c6f6216c52b25911a3b1d Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sun, 27 Jan 2019 15:43:32 +0000 Subject: Add Manager struct to manage Nitrokey connections As part of the connection refactoring, we introduce the Manager struct that deals with connection management. To make sure there can be only once instance of the manager, we add a global static Mutex that holds the single Manager instance. We use the struct to ensure that the user can only connect to one device at a time. This also changes the Error::PoisonError variant to store the sync::PoisonError. This allows the user to call into_inner on the PoisonError to retrieve the MutexGuard and to ignore the error (for example useful during testing). --- src/lib.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) (limited to 'src/lib.rs') diff --git a/src/lib.rs b/src/lib.rs index c35829c..573f45f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,9 @@ #![warn(missing_docs, rust_2018_compatibility, rust_2018_idioms, unused)] +#[macro_use(lazy_static)] +extern crate lazy_static; + mod auth; mod config; mod device; @@ -98,6 +101,8 @@ mod pws; mod util; use std::fmt; +use std::marker; +use std::sync; use nitrokey_sys; @@ -117,6 +122,10 @@ pub const DEFAULT_ADMIN_PIN: &str = "12345678"; /// The default user PIN for all Nitrokey devices. pub const DEFAULT_USER_PIN: &str = "123456"; +lazy_static! { + static ref MANAGER: sync::Mutex = sync::Mutex::new(Manager::new()); +} + /// A version of the libnitrokey library. /// /// Use the [`get_library_version`](fn.get_library_version.html) function to query the library @@ -147,6 +156,63 @@ impl fmt::Display for Version { } } +/// A manager for connections to Nitrokey devices. +/// +/// Currently, libnitrokey only provides access to one Nitrokey device at the same time. This +/// manager struct makes sure that `nitrokey-rs` does not try to connect to two devices at the same +/// time. +/// +/// To obtain an instance of this manager, use the [`take`][] function. +/// +/// [`take`]: fn.take.html +#[derive(Debug)] +pub struct Manager { + marker: marker::PhantomData<()>, +} + +impl Manager { + fn new() -> Self { + Manager { + marker: marker::PhantomData, + } + } +} + +/// Take an instance of the connection manager, blocking until an instance is available. +/// +/// There may only be one [`Manager`][] instance at the same time. If there already is an +/// instance, this method blocks. If you want a non-blocking version, use [`take`][]. +/// +/// # Errors +/// +/// - [`PoisonError`][] if the lock is poisoned +/// +/// [`take`]: fn.take.html +/// [`PoisonError`]: struct.Error.html#variant.PoisonError +/// [`Manager`]: struct.Manager.html +pub fn take_blocking() -> Result, Error> { + MANAGER.lock().map_err(Into::into) +} + +/// Try to take an instance of the connection manager. +/// +/// There may only be one [`Manager`][] instance at the same time. If there already is an +/// instance, a [`ConcurrentAccessError`][] is returned. If you want a blocking version, use +/// [`take_blocking`][]. +/// +/// # Errors +/// +/// - [`ConcurrentAccessError`][] if the token for the `Manager` instance cannot be locked +/// - [`PoisonError`][] if the lock is poisoned +/// +/// [`take_blocking`]: fn.take_blocking.html +/// [`ConcurrentAccessError`]: struct.Error.html#variant.ConcurrentAccessError +/// [`PoisonError`]: struct.Error.html#variant.PoisonError +/// [`Manager`]: struct.Manager.html +pub fn take() -> Result, Error> { + MANAGER.try_lock().map_err(Into::into) +} + /// 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`][]). -- cgit v1.2.1 From 54d23475aa3b712a539bad129fe37223173268f2 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sun, 27 Jan 2019 17:44:59 +0000 Subject: Move the connect function into Manager As part of the connection refactoring, we replace the connect function with the Manager::connect method. To maintain compatibility with nitrokey-test, the connect function is not removed but marked as deprecated. --- src/lib.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) (limited to 'src/lib.rs') diff --git a/src/lib.rs b/src/lib.rs index 573f45f..9f88be3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,6 +108,7 @@ use nitrokey_sys; pub use crate::auth::{Admin, Authenticate, User}; pub use crate::config::Config; +#[allow(deprecated)] pub use crate::device::{ connect, connect_model, Device, DeviceWrapper, Model, Pro, SdCardData, Storage, StorageProductionInfo, StorageStatus, VolumeMode, VolumeStatus, @@ -176,6 +177,43 @@ impl Manager { marker: marker::PhantomData, } } + + /// 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 + /// + /// - [`NotConnected`][] if no Nitrokey device is connected + /// + /// # Example + /// + /// ``` + /// use nitrokey::DeviceWrapper; + /// + /// fn do_something(device: DeviceWrapper) {} + /// + /// # fn main() -> Result<(), nitrokey::Error> { + /// match nitrokey::take()?.connect() { + /// Ok(device) => do_something(device), + /// Err(err) => println!("Could not connect to a Nitrokey: {}", err), + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + pub fn connect(&mut self) -> Result { + if unsafe { nitrokey_sys::NK_login_auto() } == 1 { + match device::get_connected_device() { + Some(wrapper) => Ok(wrapper), + None => Err(CommunicationError::NotConnected.into()), + } + } else { + Err(CommunicationError::NotConnected.into()) + } + } } /// Take an instance of the connection manager, blocking until an instance is available. -- cgit v1.2.1 From bd7c7a5fdf0ae66a1ff2f00beb5ed4c2e6994ca1 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sun, 27 Jan 2019 18:07:59 +0000 Subject: Move the connect_model function into Manager As part of the connection refactoring, this patch moves the connect_model function to the Manager struct. As the connect_model function is not used by nitrokey-test, it is removed. --- src/lib.rs | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) (limited to 'src/lib.rs') diff --git a/src/lib.rs b/src/lib.rs index 9f88be3..784dc6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,8 +110,8 @@ pub use crate::auth::{Admin, Authenticate, User}; pub use crate::config::Config; #[allow(deprecated)] pub use crate::device::{ - connect, connect_model, Device, DeviceWrapper, Model, Pro, SdCardData, Storage, - StorageProductionInfo, StorageStatus, VolumeMode, VolumeStatus, + connect, Device, DeviceWrapper, Model, Pro, SdCardData, Storage, StorageProductionInfo, + StorageStatus, VolumeMode, VolumeStatus, }; pub use crate::error::{CommandError, CommunicationError, Error, LibraryError}; pub use crate::otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; @@ -214,6 +214,38 @@ impl Manager { Err(CommunicationError::NotConnected.into()) } } + + /// Connects to a Nitrokey device of the given model. + /// + /// # Errors + /// + /// - [`NotConnected`][] if no Nitrokey device of the given model is connected + /// + /// # Example + /// + /// ``` + /// use nitrokey::DeviceWrapper; + /// use nitrokey::Model; + /// + /// fn do_something(device: DeviceWrapper) {} + /// + /// # fn main() -> Result<(), nitrokey::Error> { + /// match nitrokey::take()?.connect_model(Model::Pro) { + /// Ok(device) => do_something(device), + /// Err(err) => println!("Could not connect to a Nitrokey Pro: {}", err), + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + pub fn connect_model(&mut self, model: Model) -> Result { + if device::connect_enum(model) { + Ok(device::create_device_wrapper(model)) + } else { + Err(CommunicationError::NotConnected.into()) + } + } } /// Take an instance of the connection manager, blocking until an instance is available. -- cgit v1.2.1 From 379bc798477a1de7ffda923c5d10ca63aebae25f Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sun, 27 Jan 2019 18:21:08 +0000 Subject: Move {Pro, Storage}::connect into Manager As part of the connection refactoring, this patch moves the connect methods of the Pro and Storage structs into the Manager struct. To maintain compatibility with nitrokey-test, the old methods are not removed but marked as deprecated. --- src/lib.rs | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) (limited to 'src/lib.rs') diff --git a/src/lib.rs b/src/lib.rs index 784dc6f..dc3432a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -246,6 +246,68 @@ impl Manager { Err(CommunicationError::NotConnected.into()) } } + + /// Connects to a Nitrokey Pro. + /// + /// # Errors + /// + /// - [`NotConnected`][] if no Nitrokey device of the given model is connected + /// + /// # Example + /// + /// ``` + /// use nitrokey::Pro; + /// + /// fn use_pro(device: Pro) {} + /// + /// # fn main() -> Result<(), nitrokey::Error> { + /// match nitrokey::take()?.connect_pro() { + /// Ok(device) => use_pro(device), + /// Err(err) => println!("Could not connect to the Nitrokey Pro: {}", err), + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + pub fn connect_pro(&mut self) -> Result { + if device::connect_enum(device::Model::Pro) { + Ok(device::Pro::new()) + } else { + Err(CommunicationError::NotConnected.into()) + } + } + + /// Connects to a Nitrokey Storage. + /// + /// # Errors + /// + /// - [`NotConnected`][] if no Nitrokey device of the given model is connected + /// + /// # Example + /// + /// ``` + /// use nitrokey::Storage; + /// + /// fn use_storage(device: Storage) {} + /// + /// # fn main() -> Result<(), nitrokey::Error> { + /// match nitrokey::take()?.connect_storage() { + /// Ok(device) => use_storage(device), + /// Err(err) => println!("Could not connect to the Nitrokey Storage: {}", err), + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + pub fn connect_storage(&mut self) -> Result { + if device::connect_enum(Model::Storage) { + Ok(Storage::new()) + } else { + Err(CommunicationError::NotConnected.into()) + } + } } /// Take an instance of the connection manager, blocking until an instance is available. -- cgit v1.2.1 From fe2f39826ade5a156945dabb8c8ab725378a15c1 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sun, 27 Jan 2019 18:42:14 +0000 Subject: Store mutable reference to Manager in Device In the last patches, we ensured that devices can only be obtained using the Manager struct. But we did not ensure that there is only one device at a time. This patch adds a mutable reference to the Manager instance to the Device implementations. The borrow checker makes sure that there is only one mutable reference at a time. In this patch, we have to remove the old connect, Pro::connect and Storage::connect functions as they do no longer compile. (They discard the MutexGuard which invalidates the reference to the Manager.) Therefore the tests do no longer compile. --- src/lib.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) (limited to 'src/lib.rs') diff --git a/src/lib.rs b/src/lib.rs index dc3432a..8b0aae5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,8 +110,8 @@ pub use crate::auth::{Admin, Authenticate, User}; pub use crate::config::Config; #[allow(deprecated)] pub use crate::device::{ - connect, Device, DeviceWrapper, Model, Pro, SdCardData, Storage, StorageProductionInfo, - StorageStatus, VolumeMode, VolumeStatus, + Device, DeviceWrapper, Model, Pro, SdCardData, Storage, StorageProductionInfo, StorageStatus, + VolumeMode, VolumeStatus, }; pub use crate::error::{CommandError, CommunicationError, Error, LibraryError}; pub use crate::otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; @@ -204,12 +204,9 @@ impl Manager { /// ``` /// /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected - pub fn connect(&mut self) -> Result { + pub fn connect(&mut self) -> Result, Error> { if unsafe { nitrokey_sys::NK_login_auto() } == 1 { - match device::get_connected_device() { - Some(wrapper) => Ok(wrapper), - None => Err(CommunicationError::NotConnected.into()), - } + device::get_connected_device(self) } else { Err(CommunicationError::NotConnected.into()) } @@ -239,9 +236,9 @@ impl Manager { /// ``` /// /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected - pub fn connect_model(&mut self, model: Model) -> Result { + pub fn connect_model(&mut self, model: Model) -> Result, Error> { if device::connect_enum(model) { - Ok(device::create_device_wrapper(model)) + Ok(device::create_device_wrapper(self, model)) } else { Err(CommunicationError::NotConnected.into()) } @@ -270,9 +267,9 @@ impl Manager { /// ``` /// /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected - pub fn connect_pro(&mut self) -> Result { + pub fn connect_pro(&mut self) -> Result, Error> { if device::connect_enum(device::Model::Pro) { - Ok(device::Pro::new()) + Ok(device::Pro::new(self)) } else { Err(CommunicationError::NotConnected.into()) } @@ -301,9 +298,9 @@ impl Manager { /// ``` /// /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected - pub fn connect_storage(&mut self) -> Result { + pub fn connect_storage(&mut self) -> Result, Error> { if device::connect_enum(Model::Storage) { - Ok(Storage::new()) + Ok(Storage::new(self)) } else { Err(CommunicationError::NotConnected.into()) } -- cgit v1.2.1 From 62e8ee8f5d02511d6eb5dc179b087b04e88c1b94 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sun, 27 Jan 2019 19:52:53 +0000 Subject: Update documentation for Manager refactoring This patch updates the documentation to reflect the latest changes to connection handling. It also updates the doc tests to prefer the new methods over the old ones. --- src/lib.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 13 deletions(-) (limited to 'src/lib.rs') diff --git a/src/lib.rs b/src/lib.rs index 8b0aae5..36a1dfd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,13 +9,16 @@ //! 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 +//! You can only connect to one Nitrokey at a time. Use the global [`take`][] function to obtain +//! an reference to the [`Manager`][] singleton that keeps track of the connections. Then use the +//! [`connect`][] method 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. +//! [`connect_model`][], [`connect_pro`][] or [`connect_storage`][] 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. +//! You can call [`authenticate_user`][] or [`authenticate_admin`][] to get an authenticated device +//! that can perform operations that require authentication. You can use [`device`][] to go back +//! to the unauthenticated device. //! //! 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 @@ -31,7 +34,8 @@ //! # use nitrokey::Error; //! //! # fn try_main() -> Result<(), Error> { -//! let device = nitrokey::connect()?; +//! let mut manager = nitrokey::take()?; +//! let device = manager.connect()?; //! println!("{}", device.get_serial_number()?); //! # Ok(()) //! # } @@ -44,7 +48,8 @@ //! # use nitrokey::Error; //! //! # fn try_main() -> Result<(), Error> { -//! let device = nitrokey::connect()?; +//! let mut manager = nitrokey::take()?; +//! let device = manager.connect()?; //! let slot_data = OtpSlotData::new(1, "test", "01234567890123456689", OtpMode::SixDigits); //! match device.authenticate_admin("12345678") { //! Ok(mut admin) => { @@ -66,7 +71,8 @@ //! # use nitrokey::Error; //! //! # fn try_main() -> Result<(), Error> { -//! let mut device = nitrokey::connect()?; +//! let mut manager = nitrokey::take()?; +//! let mut device = manager.connect()?; //! match device.get_hotp_code(1) { //! Ok(code) => println!("Generated HOTP code: {}", code), //! Err(err) => eprintln!("Could not generate HOTP code: {}", err), @@ -77,9 +83,12 @@ //! //! [`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 +//! [`take`]: fn.take.html +//! [`connect`]: struct.Manager.html#method.connect +//! [`connect_model`]: struct.Manager.html#method.connect_model +//! [`connect_pro`]: struct.Manager.html#method.connect_pro +//! [`connect_storage`]: struct.Manager.html#method.connect_storage +//! [`manager`]: trait.Device.html#method.manager //! [`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 @@ -163,9 +172,50 @@ impl fmt::Display for Version { /// manager struct makes sure that `nitrokey-rs` does not try to connect to two devices at the same /// time. /// -/// To obtain an instance of this manager, use the [`take`][] function. +/// To obtain a reference to an instance of this manager, use the [`take`][] function. Use one of +/// the connect methods – [`connect`][], [`connect_model`][], [`connect_pro`][] or +/// [`connect_storage`][] – to retrieve a [`Device`][] instance. /// +/// # Examples +/// +/// Connect to a single device: +/// +/// ```no_run +/// use nitrokey::Device; +/// # use nitrokey::Error; +/// +/// # fn try_main() -> Result<(), Error> { +/// let mut manager = nitrokey::take()?; +/// let device = manager.connect()?; +/// println!("{}", device.get_serial_number()?); +/// # Ok(()) +/// # } +/// ``` +/// +/// Connect to a Pro and a Storage device: +/// +/// ```no_run +/// use nitrokey::{Device, Model}; +/// # use nitrokey::Error; +/// +/// # fn try_main() -> Result<(), Error> { +/// let mut manager = nitrokey::take()?; +/// let device = manager.connect_model(Model::Pro)?; +/// println!("Pro: {}", device.get_serial_number()?); +/// drop(device); +/// let device = manager.connect_model(Model::Storage)?; +/// println!("Storage: {}", device.get_serial_number()?); +/// # Ok(()) +/// # } +/// ``` +/// +/// [`connect`]: #method.connect +/// [`connect_model`]: #method.connect_model +/// [`connect_pro`]: #method.connect_pro +/// [`connect_storage`]: #method.connect_storage +/// [`manager`]: trait.Device.html#method.manager /// [`take`]: fn.take.html +/// [`Device`]: trait.Device.html #[derive(Debug)] pub struct Manager { marker: marker::PhantomData<()>, @@ -195,7 +245,8 @@ impl Manager { /// fn do_something(device: DeviceWrapper) {} /// /// # fn main() -> Result<(), nitrokey::Error> { - /// match nitrokey::take()?.connect() { + /// let mut manager = nitrokey::take()?; + /// match manager.connect() { /// Ok(device) => do_something(device), /// Err(err) => println!("Could not connect to a Nitrokey: {}", err), /// } -- cgit v1.2.1