diff options
Diffstat (limited to 'src/lib.rs')
-rw-r--r-- | src/lib.rs | 481 |
1 files changed, 452 insertions, 29 deletions
@@ -1,3 +1,6 @@ +// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org> +// SPDX-License-Identifier: MIT + //! Provides access to a Nitrokey device using the native libnitrokey API. //! //! # Usage @@ -6,29 +9,46 @@ //! 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. +//! +//! To get a list of all connected Nitrokey devices, use the [`list_devices`][] function. You can +//! then connect to one of the connected devices using the [`connect_path`][] function of the +//! `Manager` struct. //! -//! You can 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 //! passwords – [`get_hotp_code`][] and [`get_totp_code`][]. Depending on the stick configuration, //! these operations are available without authentication or with user authentication. //! +//! # Background operations +//! +//! Some commands may start background operations. During such an operation, every new command +//! will cause a [`WrongCrc`][] error. To check whether a background operation is currently +//! running, use the [`get_operation_status`][] method. +//! +//! Background operations are only available on the Nitrokey Storage. Currently, +//! [`fill_sd_card`][] is the only command that triggers a background operation. +//! //! # Examples //! //! Connect to any Nitrokey and print its serial number: //! //! ```no_run //! use nitrokey::Device; -//! # use nitrokey::CommandError; +//! # use nitrokey::Error; //! -//! # fn try_main() -> Result<(), CommandError> { -//! let device = nitrokey::connect()?; +//! # fn try_main() -> Result<(), Error> { +//! let mut manager = nitrokey::take()?; +//! let device = manager.connect()?; //! println!("{}", device.get_serial_number()?); //! # Ok(()) //! # } @@ -38,19 +58,20 @@ //! //! ```no_run //! use nitrokey::{Authenticate, ConfigureOtp, OtpMode, OtpSlotData}; -//! # use nitrokey::CommandError; +//! # use nitrokey::Error; //! -//! # fn try_main() -> Result<(), (CommandError)> { -//! let device = nitrokey::connect()?; +//! # fn try_main() -> Result<(), Error> { +//! 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(admin) => { +//! Ok(mut admin) => { //! match admin.write_hotp_slot(slot_data, 0) { //! Ok(()) => println!("Successfully wrote slot."), -//! Err(err) => println!("Could not write slot: {}", err), +//! Err(err) => eprintln!("Could not write slot: {}", err), //! } //! }, -//! Err((_, err)) => println!("Could not authenticate as admin: {}", err), +//! Err((_, err)) => eprintln!("Could not authenticate as admin: {}", err), //! } //! # Ok(()) //! # } @@ -60,13 +81,14 @@ //! //! ```no_run //! use nitrokey::{Device, GenerateOtp}; -//! # use nitrokey::CommandError; +//! # use nitrokey::Error; //! -//! # fn try_main() -> Result<(), (CommandError)> { -//! let device = nitrokey::connect()?; +//! # fn try_main() -> Result<(), Error> { +//! 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) => println!("Could not generate HOTP code: {}", err), +//! Err(err) => eprintln!("Could not generate HOTP code: {}", err), //! } //! # Ok(()) //! # } @@ -74,36 +96,64 @@ //! //! [`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_path`]: struct.Manager.html#method.connect_path +//! [`connect_pro`]: struct.Manager.html#method.connect_pro +//! [`connect_storage`]: struct.Manager.html#method.connect_storage +//! [`fill_sd_card`]: struct.Storage.html#method.fill_sd_card +//! [`get_operation_status`]: struct.Storage.html#method.get_operation_status +//! [`list_devices`]: fn.list_devices.html +//! [`manager`]: trait.Device.html#method.manager //! [`device`]: struct.User.html#method.device //! [`get_hotp_code`]: trait.GenerateOtp.html#method.get_hotp_code //! [`get_totp_code`]: trait.GenerateOtp.html#method.get_totp_code //! [`Admin`]: struct.Admin.html //! [`DeviceWrapper`]: enum.DeviceWrapper.html //! [`User`]: struct.User.html +//! [`WrongCrc`]: enum.CommandError.html#variant.WrongCrc #![warn(missing_docs, rust_2018_compatibility, rust_2018_idioms, unused)] +#[macro_use(lazy_static)] +extern crate lazy_static; + mod auth; mod config; mod device; +mod error; mod otp; mod pws; mod util; -use nitrokey_sys; +use std::convert::TryInto as _; +use std::fmt; +use std::marker; +use std::ptr::NonNull; +use std::sync; 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, + Device, DeviceInfo, DeviceWrapper, Model, OperationStatus, Pro, SdCardData, SerialNumber, + Status, Storage, StorageProductionInfo, StorageStatus, VolumeMode, VolumeStatus, }; +pub use crate::error::{CommandError, CommunicationError, Error, LibraryError}; pub use crate::otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; pub use crate::pws::{GetPasswordSafe, PasswordSafe, SLOT_COUNT}; -pub use crate::util::{CommandError, LogLevel}; +pub use crate::util::LogLevel; + +use crate::util::{get_cstring, get_last_result}; + +/// The default admin PIN for all Nitrokey devices. +pub const DEFAULT_ADMIN_PIN: &str = "12345678"; +/// The default user PIN for all Nitrokey devices. +pub const DEFAULT_USER_PIN: &str = "123456"; + +lazy_static! { + static ref MANAGER: sync::Mutex<Manager> = sync::Mutex::new(Manager::new()); +} /// A version of the libnitrokey library. /// @@ -125,6 +175,372 @@ pub struct Version { pub minor: u32, } +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.git.is_empty() { + write!(f, "v{}.{}", self.major, self.minor) + } else { + f.write_str(&self.git) + } + } +} + +/// 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 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<()>, +} + +impl Manager { + fn new() -> Self { + 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 + /// - [`UnsupportedModelError`][] if the Nitrokey device is not supported by this crate + /// + /// # Example + /// + /// ``` + /// use nitrokey::DeviceWrapper; + /// + /// fn do_something(device: DeviceWrapper) {} + /// + /// let mut manager = nitrokey::take()?; + /// match manager.connect() { + /// Ok(device) => do_something(device), + /// Err(err) => println!("Could not connect to a Nitrokey: {}", err), + /// } + /// # Ok::<(), nitrokey::Error>(()) + /// ``` + /// + /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + /// [`UnsupportedModelError`]: enum.Error.html#variant.UnsupportedModelError + pub fn connect(&mut self) -> Result<DeviceWrapper<'_>, Error> { + if unsafe { nitrokey_sys::NK_login_auto() } == 1 { + device::get_connected_device(self) + } else { + 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) {} + /// + /// match nitrokey::take()?.connect_model(Model::Pro) { + /// Ok(device) => do_something(device), + /// Err(err) => println!("Could not connect to a Nitrokey Pro: {}", err), + /// } + /// # Ok::<(), nitrokey::Error>(()) + /// ``` + /// + /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + pub fn connect_model(&mut self, model: Model) -> Result<DeviceWrapper<'_>, Error> { + if device::connect_enum(model) { + Ok(device::create_device_wrapper(self, model)) + } else { + Err(CommunicationError::NotConnected.into()) + } + } + + /// Connects to a Nitrokey device at the given USB path. + /// + /// To get a list of all connected Nitrokey devices, use the [`list_devices`][] function. The + /// [`DeviceInfo`][] structs returned by that function contain the USB path in the `path` + /// field. + /// + /// # Errors + /// + /// - [`InvalidString`][] if the USB path contains a null byte + /// - [`NotConnected`][] if no Nitrokey device can be found at the given USB path + /// - [`UnsupportedModelError`][] if the model of the Nitrokey device at the given USB path is + /// not supported by this crate + /// + /// # Example + /// + /// ``` + /// use nitrokey::DeviceWrapper; + /// + /// fn use_device(device: DeviceWrapper) {} + /// + /// let mut manager = nitrokey::take()?; + /// let devices = nitrokey::list_devices()?; + /// for device in devices { + /// let device = manager.connect_path(device.path)?; + /// use_device(device); + /// } + /// # Ok::<(), nitrokey::Error>(()) + /// ``` + /// + /// [`list_devices`]: fn.list_devices.html + /// [`DeviceInfo`]: struct.DeviceInfo.html + /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString + /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + /// [`UnsupportedModelError`]: enum.Error.html#variant.UnsupportedModelError + pub fn connect_path<S: Into<Vec<u8>>>(&mut self, path: S) -> Result<DeviceWrapper<'_>, Error> { + let path = get_cstring(path)?; + if unsafe { nitrokey_sys::NK_connect_with_path(path.as_ptr()) } == 1 { + device::get_connected_device(self) + } else { + Err(CommunicationError::NotConnected.into()) + } + } + + /// Connects to a Nitrokey Pro. + /// + /// # Errors + /// + /// - [`NotConnected`][] if no Nitrokey device of the given model is connected + /// + /// # Example + /// + /// ``` + /// use nitrokey::Pro; + /// + /// fn use_pro(device: Pro) {} + /// + /// match nitrokey::take()?.connect_pro() { + /// Ok(device) => use_pro(device), + /// Err(err) => println!("Could not connect to the Nitrokey Pro: {}", err), + /// } + /// # Ok::<(), nitrokey::Error>(()) + /// ``` + /// + /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + pub fn connect_pro(&mut self) -> Result<Pro<'_>, Error> { + if device::connect_enum(device::Model::Pro) { + Ok(device::Pro::new(self)) + } 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) {} + /// + /// match nitrokey::take()?.connect_storage() { + /// Ok(device) => use_storage(device), + /// Err(err) => println!("Could not connect to the Nitrokey Storage: {}", err), + /// } + /// # Ok::<(), nitrokey::Error>(()) + /// ``` + /// + /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + pub fn connect_storage(&mut self) -> Result<Storage<'_>, Error> { + if device::connect_enum(Model::Storage) { + Ok(Storage::new(self)) + } else { + Err(CommunicationError::NotConnected.into()) + } + } +} + +/// 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<sync::MutexGuard<'static, Manager>, 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`][]. If you want to access the manager instance even if the cache is poisoned, +/// use [`force_take`][]. +/// +/// # Errors +/// +/// - [`ConcurrentAccessError`][] if the token for the `Manager` instance cannot be locked +/// - [`PoisonError`][] if the lock is poisoned +/// +/// [`take_blocking`]: fn.take_blocking.html +/// [`force_take`]: fn.force_take.html +/// [`ConcurrentAccessError`]: struct.Error.html#variant.ConcurrentAccessError +/// [`PoisonError`]: struct.Error.html#variant.PoisonError +/// [`Manager`]: struct.Manager.html +pub fn take() -> Result<sync::MutexGuard<'static, Manager>, Error> { + MANAGER.try_lock().map_err(Into::into) +} + +/// Try to take an instance of the connection manager, ignoring a poisoned cache. +/// +/// 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`][]. +/// +/// If a thread has previously panicked while accessing the manager instance, the cache is +/// poisoned. The default implementation, [`take`][], returns a [`PoisonError`][] on subsequent +/// calls. This implementation ignores the poisoned cache and returns the manager instance. +/// +/// # Errors +/// +/// - [`ConcurrentAccessError`][] if the token for the `Manager` instance cannot be locked +/// +/// [`take`]: fn.take.html +/// [`take_blocking`]: fn.take_blocking.html +/// [`ConcurrentAccessError`]: struct.Error.html#variant.ConcurrentAccessError +/// [`Manager`]: struct.Manager.html +pub fn force_take() -> Result<sync::MutexGuard<'static, Manager>, Error> { + MANAGER.try_lock().or_else(|err| match err { + sync::TryLockError::Poisoned(err) => Ok(err.into_inner()), + sync::TryLockError::WouldBlock => Err(Error::ConcurrentAccessError), + }) +} + +/// List all connected Nitrokey devices. +/// +/// This functions returns a vector with [`DeviceInfo`][] structs that contain information about +/// all connected Nitrokey devices. It will even list unsupported models, although you cannot +/// connect to them. To connect to a supported model, call the [`connect_path`][] function. +/// +/// # Errors +/// +/// - [`NotConnected`][] if a Nitrokey device has been disconnected during enumeration +/// - [`Utf8Error`][] if the USB path or the serial number returned by libnitrokey are invalid +/// UTF-8 strings +/// +/// # Example +/// +/// ``` +/// let devices = nitrokey::list_devices()?; +/// if devices.is_empty() { +/// println!("No connected Nitrokey devices found."); +/// } else { +/// println!("model\tpath\tserial number"); +/// for device in devices { +/// match device.model { +/// Some(model) => print!("{}", model), +/// None => print!("unsupported"), +/// } +/// print!("\t{}\t", device.path); +/// match device.serial_number { +/// Some(serial_number) => println!("{}", serial_number), +/// None => println!("unknown"), +/// } +/// } +/// } +/// # Ok::<(), nitrokey::Error>(()) +/// ``` +/// +/// [`connect_path`]: struct.Manager.html#fn.connect_path +/// [`DeviceInfo`]: struct.DeviceInfo.html +/// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected +/// [`Utf8Error`]: enum.Error.html#variant.Utf8Error +pub fn list_devices() -> Result<Vec<DeviceInfo>, Error> { + let ptr = NonNull::new(unsafe { nitrokey_sys::NK_list_devices() }); + match ptr { + Some(mut ptr) => { + let mut vec: Vec<DeviceInfo> = Vec::new(); + push_device_info(&mut vec, unsafe { ptr.as_ref() })?; + unsafe { + nitrokey_sys::NK_free_device_info(ptr.as_mut()); + } + Ok(vec) + } + None => get_last_result().map(|_| Vec::new()), + } +} + +fn push_device_info( + vec: &mut Vec<DeviceInfo>, + info: &nitrokey_sys::NK_device_info, +) -> Result<(), Error> { + vec.push(info.try_into()?); + if let Some(ptr) = NonNull::new(info.next) { + push_device_info(vec, unsafe { ptr.as_ref() })?; + } + Ok(()) +} + /// Enables or disables debug output. Calling this method with `true` is equivalent to setting the /// log level to `Debug`; calling it with `false` is equivalent to the log level `Error` (see /// [`set_log_level`][]). @@ -149,21 +565,28 @@ pub fn set_log_level(level: LogLevel) { /// Returns the libnitrokey library version. /// +/// # Errors +/// +/// - [`Utf8Error`][] if libnitrokey returned an invalid UTF-8 string +/// /// # Example /// /// ``` -/// let version = nitrokey::get_library_version(); +/// let version = nitrokey::get_library_version()?; /// println!("Using libnitrokey {}", version.git); +/// # Ok::<(), nitrokey::Error>(()) /// ``` -pub fn get_library_version() -> Version { +/// +/// [`Utf8Error`]: enum.Error.html#variant.Utf8Error +pub fn get_library_version() -> Result<Version, Error> { // 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) + 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 } + Ok(Version { git, major, minor }) } |