aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.builds/archlinux-msrv.yml22
-rw-r--r--.builds/archlinux-use-system-lib.yml10
-rw-r--r--.builds/archlinux.yml2
-rw-r--r--.builds/lint.yml25
-rw-r--r--.gitignore3
-rw-r--r--CHANGELOG.md63
-rw-r--r--Cargo.toml9
-rw-r--r--LICENSES/CC0-1.0.txt121
-rw-r--r--LICENSES/MIT.txt (renamed from LICENSE)0
-rw-r--r--README.md65
-rw-r--r--TODO.md20
-rw-r--r--examples/list-devices.rs26
-rw-r--r--examples/otp.rs43
-rw-r--r--src/auth.rs51
-rw-r--r--src/config.rs68
-rw-r--r--src/device/mod.rs792
-rw-r--r--src/device/pro.rs93
-rw-r--r--src/device/storage.rs (renamed from src/device.rs)842
-rw-r--r--src/device/wrapper.rs142
-rw-r--r--src/error.rs63
-rw-r--r--src/lib.rs167
-rw-r--r--src/otp.rs10
-rw-r--r--src/pws.rs3
-rw-r--r--src/util.rs66
-rw-r--r--tests/device.rs143
-rw-r--r--tests/otp.rs8
-rw-r--r--tests/pws.rs2
27 files changed, 1964 insertions, 895 deletions
diff --git a/.builds/archlinux-msrv.yml b/.builds/archlinux-msrv.yml
new file mode 100644
index 0000000..b98a5dc
--- /dev/null
+++ b/.builds/archlinux-msrv.yml
@@ -0,0 +1,22 @@
+# Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
+# SPDX-License-Identifier: CC0-1.0
+image: archlinux
+packages:
+ - rustup
+ - libnitrokey
+environment:
+ USE_SYSTEM_LIBNITROKEY: "1"
+sources:
+ - https://git.ireas.org/nitrokey-rs
+tasks:
+ - setup: |
+ rustup set profile minimal
+ rustup default 1.40.0
+ - version: |
+ rustc -V
+ - build: |
+ cd nitrokey-rs
+ cargo build --release
+ - test: |
+ cd nitrokey-rs
+ cargo test
diff --git a/.builds/archlinux-use-system-lib.yml b/.builds/archlinux-use-system-lib.yml
index ac0fc0f..29bda19 100644
--- a/.builds/archlinux-use-system-lib.yml
+++ b/.builds/archlinux-use-system-lib.yml
@@ -1,5 +1,5 @@
# Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
-# SPDX-License-Identifier: MIT
+# SPDX-License-Identifier: CC0-1.0
image: archlinux
packages:
- rust
@@ -9,9 +9,17 @@ environment:
sources:
- https://git.ireas.org/nitrokey-rs
tasks:
+ - version: |
+ rustc -V
- build: |
cd nitrokey-rs
cargo build --release
- test: |
cd nitrokey-rs
cargo test
+ - format: |
+ cd nitrokey-rs
+ cargo fmt -- --check
+ - clippy: |
+ cd nitrokey-rs
+ cargo clippy -- -D warnings
diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml
index dfe2639..151eb66 100644
--- a/.builds/archlinux.yml
+++ b/.builds/archlinux.yml
@@ -1,5 +1,5 @@
# Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
-# SPDX-License-Identifier: MIT
+# SPDX-License-Identifier: CC0-1.0
image: archlinux
packages:
- rust
diff --git a/.builds/lint.yml b/.builds/lint.yml
index 86a27cd..678cde8 100644
--- a/.builds/lint.yml
+++ b/.builds/lint.yml
@@ -1,27 +1,16 @@
# Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
-# SPDX-License-Identifier: MIT
+# SPDX-License-Identifier: CC0-1.0
image: archlinux
packages:
- - rustup
- - python
- - python-pip
- - python-pygit2
+ - gnupg
+ - reuse
sources:
- https://git.ireas.org/nitrokey-rs
tasks:
- - setup: |
- pip install --user fsfe-reuse
- rustup update stable
- rustup self upgrade-data
- rustup default stable
- rustup component add rustfmt
- rustup component add clippy
- - format: |
+ - verify: |
cd nitrokey-rs
- cargo fmt -- --check
+ curl -s "https://pgp.ireas.org/0x6D533958F070C57C.txt" | gpg --import
+ git verify-commit HEAD
- reuse: |
cd nitrokey-rs
- ~/.local/bin/reuse lint
- - clippy: |
- cd nitrokey-rs
- cargo clippy -- -D warnings
+ reuse lint
diff --git a/.gitignore b/.gitignore
index 4cdf3b3..7ea18df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
-
+# Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
+# SPDX-License-Identifier: CC0-1.0
/target
/nitrokey-sys/target
**/*.rs.bk
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 41e46a6..275ea81 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,64 @@
<!---
-Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
-SPDX-License-Identifier: MIT
+Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org>
+SPDX-License-Identifier: CC0-1.0
-->
# Unreleased
+- Export the `FirmwareVersion` struct.
+- Mark the `Error`, `CommandError`, `CommunicationError`, `LibraryError`,
+ `Model` and `DeviceWrapper` enums as non-exhaustive.
+ - Bump the MSRV to 1.40.0.
+- Rename the `numlock`, `capslock`, `scrollock` fields of the `Config` struct
+ to `num_lock`, `caps_lock`, `scroll_lock`.
+
+# v0.7.1 (2020-08-30)
+- Remove the custom `std::error::Error::source` implementation for
+ `error::Error` to avoid duplicate error messages.
+
+# v0.7.0 (2020-07-14)
+- Refactor the `Error` enum so that it is `Send`, `Sync` and `'static`:
+ - Remove the `sync::PoisonError` from the `PoisonError` variant.
+ - Remove `Error::RandError` variant.
+
+# v0.6.0 (2020-02-03)
+- Add `String` value to the `Error::UnexpectedError` variant.
+- Always store serial numbers as structs:
+ - Introduce the `SerialNumber` struct.
+ - Change the return type of `Device::get_serial_number` from `Result<String,
+ _>` to `Result<SerialNumber, _>`.
+ - Change the type of the field `DeviceInfo.serial_number` from
+ `Option<String>` to `Option<SerialNumber>`.
+- Use `NK_get_status` instead of `NK_read_config` to implement the
+ `Device::get_config` function.
+
+# v0.5.2 (2020-01-28)
+- Use `CString` to store the temporary password instead of `Vec<u8>`.
+- Regenerate temporary passwords if they would contain a null byte.
+
+# v0.5.1 (2020-01-15)
+- Fix serial number formatting for Nitrokey Pro devices with firmware 0.8 or
+ older in the `list_devices` function.
+
+# v0.5.0 (2020-01-14)
+- List these libnitrokey functions as unsupported:
+ - `NK_change_firmware_password_pro`
+ - `NK_connect_with_ID`
+ - `NK_enable_firmware_update_pro`
+ - `NK_list_devices_by_cpuID`
+ - `NK_send_startup`
+- Implement connection by path:
+ - Add the `Error::UnsupportedDeviceError` variant.
+ - Add the `DeviceInfo` struct.
+ - Add the `list_devices` function.
+ - Add the `connect_path` function to the `Manager` struct.
+- Add the `get_status` function to the `Device` trait.
+- Rename `Status::get_status` to `get_storage_status`.
+- Add the `get_sd_card_usage` function to the `Storage` struct.
+- Add the `OperationStatus` enum and the `get_operation_status` function for
+ the `Storage` struct.
+- Add the `fill_sd_card` function to the `Storage` struct.
+
+# v0.4.0 (2020-01-02)
- Remove the `test-pro` and `test-storage` features.
- Implement `Display` for `Version`.
- Introduce `DEFAULT_ADMIN_PIN` and `DEFAULT_USER_PIN` constants.
@@ -51,6 +106,10 @@ SPDX-License-Identifier: MIT
- Add the `into_manager` function to the `Device` trait.
- Add the `force_take` function that ignores a `PoisonError` when accessing
the manager instance.
+- Internally refactor the `device` module into submodules.
+
+# v0.3.5 (2019-12-16)
+- Update the nitrokey-sys dependency version specification to ~3.4.
# v0.3.4 (2019-01-20)
- Fix authentication methods that assumed that `char` is signed.
diff --git a/Cargo.toml b/Cargo.toml
index a912859..6eb52eb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,9 +1,9 @@
-# Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
-# SPDX-License-Identifier: MIT
+# Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org>
+# SPDX-License-Identifier: CC0-1.0
[package]
name = "nitrokey"
-version = "0.4.0-alpha.3"
+version = "0.7.1"
authors = ["Robin Krahl <robin.krahl@ireas.org>"]
edition = "2018"
homepage = "https://code.ireas.org/nitrokey-rs/"
@@ -20,8 +20,7 @@ exclude = [".builds/*"]
lazy_static = "1.2"
libc = "0.2"
nitrokey-sys = "3.5"
-rand_core = {version = "0.5", default-features = false, features = ["std"] }
-rand_os = {version = "0.2"}
+rand_core = {version = "0.5.1", features = ["getrandom"] }
[dev-dependencies]
nitrokey-test = "0.3"
diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt
new file mode 100644
index 0000000..0e259d4
--- /dev/null
+++ b/LICENSES/CC0-1.0.txt
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
diff --git a/LICENSE b/LICENSES/MIT.txt
index 6c67cd5..6c67cd5 100644
--- a/LICENSE
+++ b/LICENSES/MIT.txt
diff --git a/README.md b/README.md
index a29ac6f..fa408c2 100644
--- a/README.md
+++ b/README.md
@@ -1,38 +1,54 @@
<!---
Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
-SPDX-License-Identifier: MIT
+SPDX-License-Identifier: CC0-1.0
-->
# nitrokey-rs
A libnitrokey wrapper for Rust providing access to Nitrokey devices.
-[Documentation][]
+## Usage
+
+For usage information, have a look at the [API reference][API reference] and at
+the [examples][] in the `examples` directory. You can also have a look at the
+[`nitrocli`][] crate, a command-line interface for Nitrokey devices that uses
+this crate.
## Compatibility
-The required [`libnitrokey`][] version is built from source. The host system
+This crate provides access to all features of the [`libnitrokey`][] C API for
+both the Nitrokey Pro and the Nitrokey Storage: general configuration, one-time
+password generation, the password safe and the secure storage on the Nitrokey
+Storage.
+
+The required `libnitrokey` version is built from source. The host system
must provide `libhidapi-libusb0` (Linux) or `libhidapi` (non-Linux) in the
default library search path. Depending on your system, you might also have to
install the [Nitrokey udev rules][].
-Currently, this crate provides access to the common features of the Nitrokey
-Pro and the Nitrokey Storage: general configuration, OTP generation and the
-password safe. Basic support for the secure storage on the Nitrokey Storage is
-available but still under development.
+If you want to use a precompiled version of `libnitrokey`, you can set the
+`USE_SYSTEM_LIBNITROKEY` environment variable during build. In this case,
+`libnitrokey` must be available in the library search path.
### Unsupported Functions
The following functions provided by `libnitrokey` are deliberately not
supported by `nitrokey-rs`:
+- `NK_connect_with_ID`, `NK_list_devices_by_cpuID`. These functions can be
+ replaced by calls to `NK_connect_with_path` and `NK_list_devices`, which
+ also have a cleaner API.
+- `NK_enable_firmware_update_pro`, `NK_change_firmware_password_pro`. These
+ functions execute commands that are not yet supported by the Nitrokey Pro
+ firmware.
- `NK_get_device_model`. We know which model we connected to, so we can
provide this information without calling `libnitrokey`.
-- `NK_is_AES_supported`. This method is no longer needed for Nitrokey devices
- with a recent firmware version.
+- `NK_is_AES_supported`. This function is no longer needed for Nitrokey
+ devices with a recent firmware version.
+- `NK_send_startup`. Currently, this function is redundant to `NK_get_time`.
- `NK_set_unencrypted_volume_rorw_pin_type_user`,
`NK_set_unencrypted_read_only`, `NK_set_unencrypted_read_write`. These
- methods are only relevant for older firmware versions (pre-v0.51). As the
+ functions are only relevant for older firmware versions (pre-v0.51). As the
Nitrokey Storage firmware can be updated easily, we do not support these
outdated versions.
- `NK_totp_get_time`, `NK_status`. These functions are deprecated.
@@ -55,14 +71,22 @@ Note that the tests assume that the device’s passwords are the factory default
(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!
+don’t want to destroy all data on any connected Nitrokey device!
+
+The test suite contains some test that take very long to execute, for example
+filling the SD card of a Nitrokey Storage with random data. These tests are
+ignored per default. Use `cargo test -- --ignored` to execute the tests.
## Acknowledgments
-Thanks to Nitrokey UG for providing a Nitrokey Storage to support the
+Thanks to Nitrokey UG for providing two Nitrokey devices to support the
development of this crate. Thanks to Daniel Mueller for contributions to
`nitrokey-rs` and for the `nitrokey-test` crate.
+## Minimum Supported Rust Version
+
+This crate supports Rust 1.40.0 or later.
+
## Contact
For bug reports, patches, feature requests or other messages, please send a
@@ -70,16 +94,21 @@ mail to [nitrokey-rs-dev@ireas.org][].
## License
-This project is licensed under the [MIT License][]. `libnitrokey` is licensed
-under the [LGPL-3.0][].
+This project is licensed under the [MIT][] license. The documentation and
+configuration files contained in this repository are licensed under the
+[Creative Commons Zero][CC0] license. You can find a copy of the license texts
+in the `LICENSES` directory. `libnitrokey` is licensed under the [LGPL-3.0][].
-`nitrokey-rs` complies with [version 2.0 of the REUSE practices][reuse].
+`nitrokey-rs` complies with [version 3.0 of the REUSE specification][reuse].
-[Documentation]: https://docs.rs/nitrokey
+[API reference]: https://docs.rs/nitrokey
+[examples]: https://git.ireas.org/nitrokey-rs/tree/examples
+[`nitrocli`]: https://github.com/d-e-s-o/nitrocli
[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
-[MIT license]: https://opensource.org/licenses/MIT
+[MIT]: https://opensource.org/licenses/MIT
+[CC0]: https://creativecommons.org/publicdomain/zero/1.0/
[LGPL-3.0]: https://opensource.org/licenses/lgpl-3.0.html
-[reuse]: https://reuse.software/practices/2.0/
+[reuse]: https://reuse.software/practices/3.0/
diff --git a/TODO.md b/TODO.md
index efa66d3..e50d354 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,26 +1,12 @@
<!---
Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
-SPDX-License-Identifier: MIT
+SPDX-License-Identifier: CC0-1.0
-->
-- Add support for the currently unsupported commands:
- - `NK_send_startup`
- - `NK_fill_SD_card_with_random_data`
- - `NK_get_SD_usage_data`
- - `NK_get_progress_bar_value`
- - `NK_list_devices_by_cpuID`
- - `NK_connect_with_ID`
- - `NK_get_status`
- - `NK_list_devices`
- - `NK_free_device_info`
- - `NK_connect_with_path`
- - `NK_enable_firmware_update_pro`
- - `NK_change_firmware_password_pro`
- Clear passwords from memory.
- Lock password safe in `PasswordSafe::drop()` (see [nitrokey-storage-firmware
issue 65][]).
-- Disable creation of multiple password safes at the same time.
-- Check timing in Storage tests.
-- Consider restructuring `device::StorageStatus`.
+- Consider only regenerating the null bytes instead of the complete password in
+ `util::generate_password`.
[nitrokey-storage-firmware issue 65]: https://github.com/Nitrokey/nitrokey-storage-firmware/issues/65
diff --git a/examples/list-devices.rs b/examples/list-devices.rs
new file mode 100644
index 0000000..0066f8c
--- /dev/null
+++ b/examples/list-devices.rs
@@ -0,0 +1,26 @@
+// Copyright (C) 2020 Robin Krahl <robin.krahl@ireas.org>
+// SPDX-License-Identifier: CC0-1.0
+
+//! Enumerates all connected Nitrokey devices and prints some information about them.
+
+use nitrokey::Device as _;
+
+fn main() -> Result<(), nitrokey::Error> {
+ let mut manager = nitrokey::take()?;
+ let device_infos = nitrokey::list_devices()?;
+ if device_infos.is_empty() {
+ println!("No Nitrokey device found");
+ } else {
+ println!("path\t\tmodel\tfirmware version\tserial number");
+ for device_info in device_infos {
+ let device = manager.connect_path(device_info.path.clone())?;
+ let model = device.get_model();
+ let status = device.get_status()?;
+ println!(
+ "{}\t{}\t{}\t\t\t{}",
+ device_info.path, model, status.firmware_version, status.serial_number
+ );
+ }
+ }
+ Ok(())
+}
diff --git a/examples/otp.rs b/examples/otp.rs
new file mode 100644
index 0000000..f2c6f3c
--- /dev/null
+++ b/examples/otp.rs
@@ -0,0 +1,43 @@
+// Copyright (C) 2020 Robin Krahl <robin.krahl@ireas.org>
+// SPDX-License-Identifier: CC0-1.0
+
+//! Connects to a Nitrokey device, configures an TOTP slot and generates a one-time password from
+//! it.
+
+use std::time;
+
+use nitrokey::{Authenticate, ConfigureOtp, Device, GenerateOtp};
+
+fn main() -> Result<(), nitrokey::Error> {
+ let mut manager = nitrokey::take()?;
+ let device = manager.connect()?;
+
+ // Configure the OTP slot (requires admin PIN)
+ let data = nitrokey::OtpSlotData::new(
+ 1,
+ "test",
+ "3132333435363738393031323334353637383930",
+ nitrokey::OtpMode::SixDigits,
+ );
+ let mut admin = device.authenticate_admin("12345678")?;
+ admin.write_totp_slot(data, 30)?;
+ let mut device = admin.device();
+
+ // Set the time for the OTP generation
+ let time = time::SystemTime::now()
+ .duration_since(time::UNIX_EPOCH)
+ .expect("Invalid system time");
+ device.set_time(time.as_secs(), true)?;
+
+ // Generate a one-time password -- depending on the configuration, we have to set the user PIN
+ let config = device.get_config()?;
+ let otp = if config.user_password {
+ let user = device.authenticate_user("123456")?;
+ user.get_totp_code(1)
+ } else {
+ device.get_totp_code(1)
+ }?;
+ println!("Generated OTP code: {}", otp);
+
+ Ok(())
+}
diff --git a/src/auth.rs b/src/auth.rs
index 0b000f7..5ca59da 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -1,13 +1,13 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT
+use std::convert::TryFrom as _;
+use std::ffi::CString;
use std::marker;
use std::ops;
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::error::Error;
@@ -116,9 +116,7 @@ pub trait Authenticate<'a> {
}
trait AuthenticatedDevice<T> {
- fn new(device: T, temp_password: Vec<u8>) -> Self;
-
- fn temp_password_ptr(&self) -> *const c_char;
+ fn new(device: T, temp_password: CString) -> Self;
}
/// A Nitrokey device with user authentication.
@@ -128,12 +126,12 @@ trait AuthenticatedDevice<T> {
/// method.
///
/// [`Authenticate`]: trait.Authenticate.html
-/// [`authenticate_admin`]: trait.Authenticate.html#method.authenticate_admin
+/// [`authenticate_user`]: trait.Authenticate.html#method.authenticate_user
/// [`device`]: #method.device
#[derive(Debug)]
pub struct User<'a, T: Device<'a>> {
device: T,
- temp_password: Vec<u8>,
+ temp_password: CString,
marker: marker::PhantomData<&'a T>,
}
@@ -149,7 +147,7 @@ pub struct User<'a, T: Device<'a>> {
#[derive(Debug)]
pub struct Admin<'a, T: Device<'a>> {
device: T,
- temp_password: Vec<u8>,
+ temp_password: CString,
marker: marker::PhantomData<&'a T>,
}
@@ -168,7 +166,7 @@ where
Err(err) => return Err((device, err)),
};
let password_ptr = password.as_ptr();
- let temp_password_ptr = temp_password.as_ptr() as *const c_char;
+ let temp_password_ptr = temp_password.as_ptr();
match callback(password_ptr, temp_password_ptr) {
0 => Ok(A::new(device, temp_password)),
rv => Err((device, Error::from(rv))),
@@ -233,29 +231,25 @@ impl<'a, T: Device<'a>> ops::DerefMut for User<'a, T> {
impl<'a, T: Device<'a>> GenerateOtp for User<'a, T> {
fn get_hotp_code(&mut self, slot: u8) -> Result<String, Error> {
result_from_string(unsafe {
- nitrokey_sys::NK_get_hotp_code_PIN(slot, self.temp_password_ptr())
+ nitrokey_sys::NK_get_hotp_code_PIN(slot, self.temp_password.as_ptr())
})
}
fn get_totp_code(&self, slot: u8) -> Result<String, Error> {
result_from_string(unsafe {
- nitrokey_sys::NK_get_totp_code_PIN(slot, 0, 0, 0, self.temp_password_ptr())
+ nitrokey_sys::NK_get_totp_code_PIN(slot, 0, 0, 0, self.temp_password.as_ptr())
})
}
}
impl<'a, T: Device<'a>> AuthenticatedDevice<T> for User<'a, T> {
- fn new(device: T, temp_password: Vec<u8>) -> Self {
+ fn new(device: T, temp_password: CString) -> Self {
User {
device,
temp_password,
marker: marker::PhantomData,
}
}
-
- fn temp_password_ptr(&self) -> *const c_char {
- self.temp_password.as_ptr() as *const c_char
- }
}
impl<'a, T: Device<'a>> ops::Deref for Admin<'a, T> {
@@ -284,7 +278,8 @@ impl<'a, T: Device<'a>> Admin<'a, T> {
///
/// # Errors
///
- /// - [`InvalidSlot`][] if the provided numlock, capslock or scrolllock slot is larger than two
+ /// - [`InvalidSlot`][] if the provided Num Lock, Caps Lock or Scroll Lock slot is larger than
+ /// two
///
/// # Example
///
@@ -312,12 +307,12 @@ impl<'a, T: Device<'a>> Admin<'a, T> {
let raw_config = RawConfig::try_from(config)?;
get_command_result(unsafe {
nitrokey_sys::NK_write_config(
- raw_config.numlock,
- raw_config.capslock,
- raw_config.scrollock,
+ raw_config.num_lock,
+ raw_config.caps_lock,
+ raw_config.scroll_lock,
raw_config.user_password,
false,
- self.temp_password_ptr(),
+ self.temp_password.as_ptr(),
)
})
}
@@ -336,7 +331,7 @@ impl<'a, T: Device<'a>> ConfigureOtp for Admin<'a, T> {
raw_data.use_enter,
raw_data.use_token_id,
raw_data.token_id.as_ptr(),
- self.temp_password_ptr(),
+ self.temp_password.as_ptr(),
)
})
}
@@ -353,36 +348,32 @@ impl<'a, T: Device<'a>> ConfigureOtp for Admin<'a, T> {
raw_data.use_enter,
raw_data.use_token_id,
raw_data.token_id.as_ptr(),
- self.temp_password_ptr(),
+ self.temp_password.as_ptr(),
)
})
}
fn erase_hotp_slot(&mut self, slot: u8) -> Result<(), Error> {
get_command_result(unsafe {
- nitrokey_sys::NK_erase_hotp_slot(slot, self.temp_password_ptr())
+ nitrokey_sys::NK_erase_hotp_slot(slot, self.temp_password.as_ptr())
})
}
fn erase_totp_slot(&mut self, slot: u8) -> Result<(), Error> {
get_command_result(unsafe {
- nitrokey_sys::NK_erase_totp_slot(slot, self.temp_password_ptr())
+ nitrokey_sys::NK_erase_totp_slot(slot, self.temp_password.as_ptr())
})
}
}
impl<'a, T: Device<'a>> AuthenticatedDevice<T> for Admin<'a, T> {
- fn new(device: T, temp_password: Vec<u8>) -> Self {
+ fn new(device: T, temp_password: CString) -> Self {
Admin {
device,
temp_password,
marker: marker::PhantomData,
}
}
-
- fn temp_password_ptr(&self) -> *const c_char {
- self.temp_password.as_ptr() as *const c_char
- }
}
impl<'a> Authenticate<'a> for DeviceWrapper<'a> {
diff --git a/src/config.rs b/src/config.rs
index c273792..bc935d7 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,20 +1,22 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT
+use std::convert;
+
use crate::error::{Error, LibraryError};
/// The configuration for a Nitrokey.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Config {
- /// If set, the stick will generate a code from the HOTP slot with the given number if numlock
- /// is pressed. The slot number must be 0, 1 or 2.
- pub numlock: Option<u8>,
- /// If set, the stick will generate a code from the HOTP slot with the given number if capslock
+ /// If set, the stick will generate a code from the HOTP slot with the given number if Num Lock
/// is pressed. The slot number must be 0, 1 or 2.
- pub capslock: Option<u8>,
- /// If set, the stick will generate a code from the HOTP slot with the given number if
- /// scrollock is pressed. The slot number must be 0, 1 or 2.
- pub scrollock: Option<u8>,
+ pub num_lock: Option<u8>,
+ /// If set, the stick will generate a code from the HOTP slot with the given number if Caps
+ /// Lock is pressed. The slot number must be 0, 1 or 2.
+ pub caps_lock: Option<u8>,
+ /// If set, the stick will generate a code from the HOTP slot with the given number if Scroll
+ /// Lock is pressed. The slot number must be 0, 1 or 2.
+ pub scroll_lock: Option<u8>,
/// If set, OTP generation using [`get_hotp_code`][] or [`get_totp_code`][] requires user
/// authentication. Otherwise, OTPs can be generated without authentication.
///
@@ -25,9 +27,9 @@ pub struct Config {
#[derive(Debug)]
pub struct RawConfig {
- pub numlock: u8,
- pub capslock: u8,
- pub scrollock: u8,
+ pub num_lock: u8,
+ pub caps_lock: u8,
+ pub scroll_lock: u8,
pub user_password: bool,
}
@@ -54,38 +56,40 @@ fn option_to_config_otp_slot(value: Option<u8>) -> Result<u8, Error> {
impl Config {
/// Constructs a new instance of this struct.
pub fn new(
- numlock: Option<u8>,
- capslock: Option<u8>,
- scrollock: Option<u8>,
+ num_lock: Option<u8>,
+ caps_lock: Option<u8>,
+ scroll_lock: Option<u8>,
user_password: bool,
) -> Config {
Config {
- numlock,
- capslock,
- scrollock,
+ num_lock,
+ caps_lock,
+ scroll_lock,
user_password,
}
}
}
-impl RawConfig {
- pub fn try_from(config: Config) -> Result<RawConfig, Error> {
+impl convert::TryFrom<Config> for RawConfig {
+ type Error = Error;
+
+ fn try_from(config: Config) -> Result<RawConfig, Error> {
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)?,
+ num_lock: option_to_config_otp_slot(config.num_lock)?,
+ caps_lock: option_to_config_otp_slot(config.caps_lock)?,
+ scroll_lock: option_to_config_otp_slot(config.scroll_lock)?,
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 From<&nitrokey_sys::NK_status> for RawConfig {
+ fn from(status: &nitrokey_sys::NK_status) -> Self {
+ Self {
+ num_lock: status.config_numlock,
+ caps_lock: status.config_capslock,
+ scroll_lock: status.config_scrolllock,
+ user_password: status.otp_user_password,
}
}
}
@@ -93,9 +97,9 @@ impl From<[u8; 5]> for RawConfig {
impl Into<Config> for RawConfig {
fn into(self) -> Config {
Config {
- numlock: config_otp_slot_to_option(self.numlock),
- capslock: config_otp_slot_to_option(self.capslock),
- scrollock: config_otp_slot_to_option(self.scrollock),
+ num_lock: config_otp_slot_to_option(self.num_lock),
+ caps_lock: config_otp_slot_to_option(self.caps_lock),
+ scroll_lock: config_otp_slot_to_option(self.scroll_lock),
user_password: self.user_password,
}
}
diff --git a/src/device/mod.rs b/src/device/mod.rs
new file mode 100644
index 0000000..7fec18b
--- /dev/null
+++ b/src/device/mod.rs
@@ -0,0 +1,792 @@
+// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
+// SPDX-License-Identifier: MIT
+
+mod pro;
+mod storage;
+mod wrapper;
+
+use std::convert::{TryFrom, TryInto};
+use std::ffi;
+use std::fmt;
+use std::str;
+
+use crate::auth::Authenticate;
+use crate::config::{Config, RawConfig};
+use crate::error::{CommunicationError, Error, LibraryError};
+use crate::otp::GenerateOtp;
+use crate::pws::GetPasswordSafe;
+use crate::util::{
+ get_command_result, get_cstring, owned_str_from_ptr, result_or_error, run_with_string,
+};
+
+pub use pro::Pro;
+pub use storage::{
+ OperationStatus, SdCardData, Storage, StorageProductionInfo, StorageStatus, VolumeMode,
+ VolumeStatus,
+};
+pub use wrapper::DeviceWrapper;
+
+/// Available Nitrokey models.
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[non_exhaustive]
+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 {
+ f.write_str(match *self {
+ Model::Pro => "Pro",
+ Model::Storage => "Storage",
+ })
+ }
+}
+
+impl From<Model> for nitrokey_sys::NK_device_model {
+ fn from(model: Model) -> Self {
+ match model {
+ Model::Storage => nitrokey_sys::NK_device_model_NK_STORAGE,
+ Model::Pro => nitrokey_sys::NK_device_model_NK_PRO,
+ }
+ }
+}
+
+impl TryFrom<nitrokey_sys::NK_device_model> for Model {
+ type Error = Error;
+
+ fn try_from(model: nitrokey_sys::NK_device_model) -> Result<Self, Error> {
+ match model {
+ nitrokey_sys::NK_device_model_NK_DISCONNECTED => {
+ Err(CommunicationError::NotConnected.into())
+ }
+ nitrokey_sys::NK_device_model_NK_PRO => Ok(Model::Pro),
+ nitrokey_sys::NK_device_model_NK_STORAGE => Ok(Model::Storage),
+ _ => Err(Error::UnsupportedModelError),
+ }
+ }
+}
+
+/// Serial number of a Nitrokey device.
+///
+/// The serial number can be formatted as a string using the [`ToString`][] trait, and it can be
+/// parsed from a string using the [`FromStr`][] trait. It can also be represented as a 32-bit
+/// unsigned integer using [`as_u32`][]. This integer is the ID of the smartcard of the Nitrokey
+/// device.
+///
+/// Neither the format of the string representation nor the integer representation are guaranteed
+/// to stay the same for new firmware versions.
+///
+/// [`as_u32`]: #method.as_u32
+/// [`FromStr`]: #impl-FromStr
+/// [`ToString`]: #impl-ToString
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct SerialNumber {
+ value: u32,
+}
+
+impl SerialNumber {
+ /// Creates an emtpty serial number.
+ ///
+ /// This function can be used to create a placeholder value or to compare a `SerialNumber`
+ /// instance with an empty serial number.
+ pub fn empty() -> Self {
+ SerialNumber::new(0)
+ }
+
+ fn new(value: u32) -> Self {
+ SerialNumber { value }
+ }
+
+ /// Returns the integer reprensentation of this serial number.
+ ///
+ /// This integer currently is the ID of the smartcard of the Nitrokey device. Upcoming
+ /// firmware versions might change the meaning of this representation, or add additional
+ /// components to the serial number.
+ // To provide a stable API even if the internal representation of SerialNumber changes, we want
+ // to borrow SerialNumber instead of copying it even if it might be less efficient.
+ #[allow(clippy::trivially_copy_pass_by_ref)]
+ pub fn as_u32(&self) -> u32 {
+ self.value
+ }
+}
+
+impl fmt::Display for SerialNumber {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{:#010x}", self.value)
+ }
+}
+
+impl str::FromStr for SerialNumber {
+ type Err = Error;
+
+ /// Try to parse a serial number from a hex string.
+ ///
+ /// The input string must be a valid hex string. Optionally, it can include a `0x` prefix.
+ ///
+ /// # Errors
+ ///
+ /// - [`InvalidHexString`][] if the given string is not a valid hex string
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use std::convert::TryFrom;
+ /// use nitrokey::{DeviceInfo, Error, SerialNumber};
+ ///
+ /// fn find_device(serial_number: &str) -> Result<Option<DeviceInfo>, Error> {
+ /// let serial_number: SerialNumber = serial_number.parse()?;
+ /// Ok(nitrokey::list_devices()?
+ /// .into_iter()
+ /// .filter(|device| device.serial_number == Some(serial_number))
+ /// .next())
+ /// }
+ ///
+ /// ```
+ ///
+ /// [`InvalidHexString`]: enum.LibraryError.html#variant.InvalidHexString
+ fn from_str(s: &str) -> Result<SerialNumber, Error> {
+ // ignore leading 0x
+ let hex_string = if s.starts_with("0x") {
+ s.split_at(2).1
+ } else {
+ s
+ };
+
+ u32::from_str_radix(hex_string, 16)
+ .map(SerialNumber::new)
+ .map_err(|_| LibraryError::InvalidHexString.into())
+ }
+}
+
+/// Connection information for a Nitrokey device.
+#[derive(Clone, Debug, PartialEq)]
+pub struct DeviceInfo {
+ /// The model of the Nitrokey device, or `None` if the model is not supported by this crate.
+ pub model: Option<Model>,
+ /// The USB device path.
+ pub path: String,
+ /// The serial number of the device, or `None` if the device does not expose its serial number.
+ pub serial_number: Option<SerialNumber>,
+}
+
+impl TryFrom<&nitrokey_sys::NK_device_info> for DeviceInfo {
+ type Error = Error;
+
+ fn try_from(device_info: &nitrokey_sys::NK_device_info) -> Result<DeviceInfo, Error> {
+ let model_result = device_info.model.try_into();
+ let model_option = model_result.map(Some).or_else(|err| match err {
+ Error::UnsupportedModelError => Ok(None),
+ _ => Err(err),
+ })?;
+ let serial_number = unsafe { ffi::CStr::from_ptr(device_info.serial_number) }
+ .to_str()
+ .map_err(Error::from)?;
+ Ok(DeviceInfo {
+ model: model_option,
+ path: owned_str_from_ptr(device_info.path)?,
+ serial_number: get_hidapi_serial_number(serial_number),
+ })
+ }
+}
+
+impl fmt::Display for DeviceInfo {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self.model {
+ Some(model) => write!(f, "Nitrokey {}", model)?,
+ None => write!(f, "Unsupported Nitrokey model")?,
+ }
+ write!(f, " at {} with ", self.path)?;
+ match self.serial_number {
+ Some(serial_number) => write!(f, "serial no. {}", serial_number),
+ None => write!(f, "an unknown serial number"),
+ }
+ }
+}
+
+/// Parses a serial number returned by hidapi.
+///
+/// If the serial number is all zero, this function returns `None`. Otherwise, it uses the last
+/// eight characters. If these are all zero, the first eight characters are used instead. The
+/// selected substring is parse as a hex string and its integer value is returned from the
+/// function. If the string cannot be parsed, this function returns `None`.
+///
+/// The reason for this behavior is that the Nitrokey Storage does not report its serial number at
+/// all (all zero value), while the Nitrokey Pro with firmware 0.9 or later writes its serial
+/// number to the last eight characters. Nitrokey Pro devices with firmware 0.8 or earlier wrote
+/// their serial number to the first eight characters.
+fn get_hidapi_serial_number(serial_number: &str) -> Option<SerialNumber> {
+ let len = serial_number.len();
+ if len < 8 {
+ // The serial number in the USB descriptor has 12 bytes, we need at least four
+ return None;
+ }
+
+ let mut iter = serial_number.char_indices().rev();
+ if let Some((i, _)) = iter.find(|(_, c)| *c != '0') {
+ let substr = if len - i < 8 {
+ // The last eight characters contain at least one non-zero character --> use them
+ serial_number.split_at(len - 8).1
+ } else {
+ // The last eight characters are all zero --> use the first eight
+ serial_number.split_at(8).0
+ };
+ substr.parse().ok()
+ } else {
+ // The serial number is all zero
+ None
+ }
+}
+
+/// A firmware version for a Nitrokey device.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct FirmwareVersion {
+ /// The major firmware version, e. g. 0 in v0.40.
+ pub major: u8,
+ /// The minor firmware version, e. g. 40 in v0.40.
+ pub minor: u8,
+}
+
+impl fmt::Display for FirmwareVersion {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "v{}.{}", self.major, self.minor)
+ }
+}
+
+/// The status information common to all Nitrokey devices.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct Status {
+ /// The firmware version of the device.
+ pub firmware_version: FirmwareVersion,
+ /// The serial number of the device.
+ pub serial_number: SerialNumber,
+ /// The configuration of the device.
+ pub config: Config,
+}
+
+impl From<nitrokey_sys::NK_status> for Status {
+ fn from(status: nitrokey_sys::NK_status) -> Self {
+ Self {
+ firmware_version: FirmwareVersion {
+ major: status.firmware_version_major,
+ minor: status.firmware_version_minor,
+ },
+ serial_number: SerialNumber::new(status.serial_number_smart_card),
+ config: RawConfig::from(&status).into(),
+ }
+ }
+}
+
+/// 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<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt::Debug {
+ /// Returns the [`Manager`][] instance that has been used to connect to this device.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use nitrokey::{Device, DeviceWrapper};
+ ///
+ /// fn do_something(device: DeviceWrapper) {
+ /// // reconnect to any device
+ /// let manager = device.into_manager();
+ /// let device = manager.connect();
+ /// // do something with the device
+ /// // ...
+ /// }
+ ///
+ /// match nitrokey::take()?.connect() {
+ /// Ok(device) => do_something(device),
+ /// Err(err) => println!("Could not connect to a Nitrokey: {}", err),
+ /// }
+ /// # Ok::<(), nitrokey::Error>(())
+ /// ```
+ ///
+ /// [`Manager`]: struct.Manager.html
+ fn into_manager(self) -> &'a mut crate::Manager;
+
+ /// Returns the model of the connected Nitrokey device.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::Device;
+ /// # use nitrokey::Error;
+ ///
+ /// # fn try_main() -> Result<(), Error> {
+ /// let mut manager = nitrokey::take()?;
+ /// let device = manager.connect()?;
+ /// println!("Connected to a Nitrokey {}", device.get_model());
+ /// # Ok(())
+ /// # }
+ fn get_model(&self) -> Model;
+
+ /// Returns the status of the Nitrokey device.
+ ///
+ /// This methods returns the status information common to all Nitrokey devices as a
+ /// [`Status`][] struct. Some models may provide more information, for example
+ /// [`get_storage_status`][] returns the [`StorageStatus`][] struct.
+ ///
+ /// # Errors
+ ///
+ /// - [`NotConnected`][] if the Nitrokey device has been disconnected
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::Device;
+ ///
+ /// let mut manager = nitrokey::take()?;
+ /// let device = manager.connect()?;
+ /// let status = device.get_status()?;
+ /// println!("Firmware version: {}", status.firmware_version);
+ /// println!("Serial number: {}", status.serial_number);
+ /// # Ok::<(), nitrokey::Error>(())
+ /// ```
+ ///
+ /// [`get_storage_status`]: struct.Storage.html#method.get_storage_status
+ /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
+ /// [`Status`]: struct.Status.html
+ /// [`StorageStatus`]: struct.StorageStatus.html
+ fn get_status(&self) -> Result<Status, Error>;
+
+ /// Returns the serial number of the Nitrokey device.
+ ///
+ /// For display purpuses, the serial number should be formatted as an 8-digit hex string.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::Device;
+ /// # use nitrokey::Error;
+ ///
+ /// # fn try_main() -> Result<(), Error> {
+ /// let mut manager = nitrokey::take()?;
+ /// let device = manager.connect()?;
+ /// match device.get_serial_number() {
+ /// Ok(number) => println!("serial no: {}", number),
+ /// Err(err) => eprintln!("Could not get serial number: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ fn get_serial_number(&self) -> Result<SerialNumber, Error> {
+ run_with_string(unsafe { nitrokey_sys::NK_device_serial_number() }, |s| {
+ s.parse()
+ })
+ }
+
+ /// 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::Error;
+ ///
+ /// # fn try_main() -> Result<(), Error> {
+ /// let mut manager = nitrokey::take()?;
+ /// let device = manager.connect()?;
+ /// let count = device.get_user_retry_count();
+ /// match device.get_user_retry_count() {
+ /// Ok(count) => println!("{} remaining authentication attempts (user)", count),
+ /// Err(err) => eprintln!("Could not get user retry count: {}", err),
+ /// }
+ /// # Ok(())
+ /// # }
+ /// ```
+ fn get_user_retry_count(&self) -> Result<u8, Error> {
+ result_or_error(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::Error;
+ ///
+ /// # fn try_main() -> Result<(), Error> {
+ /// let mut manager = nitrokey::take()?;
+ /// let device = manager.connect()?;
+ /// let count = device.get_admin_retry_count();
+ /// match device.get_admin_retry_count() {
+ /// Ok(count) => println!("{} remaining authentication attempts (admin)", count),
+ /// Err(err) => eprintln!("Could not get admin retry count: {}", err),
+ /// }
+ /// # Ok(())
+ /// # }
+ /// ```
+ fn get_admin_retry_count(&self) -> Result<u8, Error> {
+ result_or_error(unsafe { nitrokey_sys::NK_get_admin_retry_count() })
+ }
+
+ /// Returns the firmware version.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::Device;
+ /// # use nitrokey::Error;
+ ///
+ /// # fn try_main() -> Result<(), Error> {
+ /// let mut manager = nitrokey::take()?;
+ /// let device = manager.connect()?;
+ /// match device.get_firmware_version() {
+ /// Ok(version) => println!("Firmware version: {}", version),
+ /// Err(err) => eprintln!("Could not access firmware version: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ fn get_firmware_version(&self) -> Result<FirmwareVersion, Error> {
+ let major = result_or_error(unsafe { nitrokey_sys::NK_get_major_firmware_version() })?;
+ let minor = result_or_error(unsafe { nitrokey_sys::NK_get_minor_firmware_version() })?;
+ Ok(FirmwareVersion { major, minor })
+ }
+
+ /// Returns the current configuration of the Nitrokey device.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::Device;
+ /// # use nitrokey::Error;
+ ///
+ /// # fn try_main() -> Result<(), Error> {
+ /// let mut manager = nitrokey::take()?;
+ /// let device = manager.connect()?;
+ /// let config = device.get_config()?;
+ /// println!("Num Lock binding: {:?}", config.num_lock);
+ /// println!("Caps Lock binding: {:?}", config.caps_lock);
+ /// println!("Scroll Lock binding: {:?}", config.scroll_lock);
+ /// println!("require password for OTP: {:?}", config.user_password);
+ /// # Ok(())
+ /// # }
+ /// ```
+ fn get_config(&self) -> Result<Config, Error> {
+ let mut raw_status = nitrokey_sys::NK_status {
+ firmware_version_major: 0,
+ firmware_version_minor: 0,
+ serial_number_smart_card: 0,
+ config_numlock: 0,
+ config_capslock: 0,
+ config_scrolllock: 0,
+ otp_user_password: false,
+ };
+ get_command_result(unsafe { nitrokey_sys::NK_get_status(&mut raw_status) })?;
+ Ok(RawConfig::from(&raw_status).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::Error;
+ ///
+ /// # fn try_main() -> Result<(), Error> {
+ /// let mut manager = nitrokey::take()?;
+ /// let mut device = manager.connect()?;
+ /// match device.change_admin_pin("12345678", "12345679") {
+ /// Ok(()) => println!("Updated admin PIN."),
+ /// Err(err) => eprintln!("Failed to update admin PIN: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
+ /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
+ fn change_admin_pin(&mut self, current: &str, new: &str) -> Result<(), Error> {
+ let current_string = get_cstring(current)?;
+ let new_string = get_cstring(new)?;
+ get_command_result(unsafe {
+ 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::Error;
+ ///
+ /// # fn try_main() -> Result<(), Error> {
+ /// let mut manager = nitrokey::take()?;
+ /// let mut device = manager.connect()?;
+ /// match device.change_user_pin("123456", "123457") {
+ /// Ok(()) => println!("Updated admin PIN."),
+ /// Err(err) => eprintln!("Failed to update admin PIN: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
+ /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
+ fn change_user_pin(&mut self, current: &str, new: &str) -> Result<(), Error> {
+ let current_string = get_cstring(current)?;
+ let new_string = get_cstring(new)?;
+ get_command_result(unsafe {
+ 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::Error;
+ ///
+ /// # fn try_main() -> Result<(), Error> {
+ /// let mut manager = nitrokey::take()?;
+ /// let mut device = manager.connect()?;
+ /// match device.unlock_user_pin("12345678", "123456") {
+ /// Ok(()) => println!("Unlocked user PIN."),
+ /// Err(err) => eprintln!("Failed to unlock user PIN: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
+ /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
+ fn unlock_user_pin(&mut self, admin_pin: &str, user_pin: &str) -> Result<(), Error> {
+ let admin_pin_string = get_cstring(admin_pin)?;
+ let user_pin_string = get_cstring(user_pin)?;
+ get_command_result(unsafe {
+ 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::Error;
+ ///
+ /// # fn try_main() -> Result<(), Error> {
+ /// let mut manager = nitrokey::take()?;
+ /// let mut device = manager.connect()?;
+ /// match device.lock() {
+ /// Ok(()) => println!("Locked the Nitrokey device."),
+ /// Err(err) => eprintln!("Could not lock the Nitrokey device: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ fn lock(&mut self) -> Result<(), Error> {
+ get_command_result(unsafe { 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::Error;
+ ///
+ /// # fn try_main() -> Result<(), Error> {
+ /// let mut manager = nitrokey::take()?;
+ /// let mut device = manager.connect()?;
+ /// match device.factory_reset("12345678") {
+ /// Ok(()) => println!("Performed a factory reset."),
+ /// Err(err) => eprintln!("Could not perform a factory reset: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
+ /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
+ /// [`build_aes_key`]: #method.build_aes_key
+ fn factory_reset(&mut self, admin_pin: &str) -> Result<(), Error> {
+ let admin_pin_string = get_cstring(admin_pin)?;
+ get_command_result(unsafe { 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::Error;
+ ///
+ /// # fn try_main() -> Result<(), Error> {
+ /// let mut manager = nitrokey::take()?;
+ /// let mut device = manager.connect()?;
+ /// match device.build_aes_key("12345678") {
+ /// Ok(()) => println!("New AES keys have been built."),
+ /// Err(err) => eprintln!("Could not build new AES keys: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
+ /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
+ /// [`factory_reset`]: #method.factory_reset
+ fn build_aes_key(&mut self, admin_pin: &str) -> Result<(), Error> {
+ let admin_pin_string = get_cstring(admin_pin)?;
+ get_command_result(unsafe { nitrokey_sys::NK_build_aes_key(admin_pin_string.as_ptr()) })
+ }
+}
+
+fn get_connected_model() -> Result<Model, Error> {
+ Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() })
+}
+
+pub(crate) fn create_device_wrapper(
+ manager: &mut crate::Manager,
+ model: Model,
+) -> DeviceWrapper<'_> {
+ match model {
+ Model::Pro => Pro::new(manager).into(),
+ Model::Storage => Storage::new(manager).into(),
+ }
+}
+
+pub(crate) fn get_connected_device(
+ manager: &mut crate::Manager,
+) -> Result<DeviceWrapper<'_>, Error> {
+ Ok(create_device_wrapper(manager, get_connected_model()?))
+}
+
+pub(crate) fn connect_enum(model: Model) -> bool {
+ unsafe { nitrokey_sys::NK_login_enum(model.into()) == 1 }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr;
+
+ use super::{get_hidapi_serial_number, LibraryError, SerialNumber};
+
+ #[test]
+ fn test_serial_number_display() {
+ fn assert_str(s: &str, n: u32) {
+ assert_eq!(s.to_owned(), SerialNumber::new(n).to_string());
+ }
+
+ assert_str("0x00000000", 0);
+ assert_str("0x00001000", 0x1000);
+ assert_str("0x12345678", 0x12345678);
+ }
+
+ #[test]
+ fn test_serial_number_try_from() {
+ fn assert_ok(v: u32, s: &str) {
+ assert_eq!(SerialNumber::new(v), SerialNumber::from_str(s).unwrap());
+ assert_eq!(
+ SerialNumber::new(v),
+ SerialNumber::from_str(format!("0x{}", s).as_ref()).unwrap()
+ );
+ }
+
+ fn assert_err(s: &str) {
+ match SerialNumber::from_str(s).unwrap_err() {
+ super::Error::LibraryError(LibraryError::InvalidHexString) => {}
+ err => assert!(
+ false,
+ "expected InvalidHexString error, got {} (input {})",
+ err, s
+ ),
+ }
+ }
+
+ assert_ok(0x1234, "1234");
+ assert_ok(0x1234, "01234");
+ assert_ok(0x1234, "001234");
+ assert_ok(0x1234, "0001234");
+
+ assert_ok(0, "0");
+ assert_ok(0xdeadbeef, "deadbeef");
+
+ assert_err("deadpork");
+ assert_err("blubb");
+ assert_err("");
+ }
+
+ #[test]
+ fn test_get_hidapi_serial_number() {
+ fn assert_none(s: &str) {
+ assert_eq!(None, get_hidapi_serial_number(s));
+ }
+
+ fn assert_some(n: u32, s: &str) {
+ assert_eq!(Some(SerialNumber::new(n)), get_hidapi_serial_number(s));
+ }
+
+ assert_none("");
+ assert_none("00000000000000000");
+ assert_none("blubb");
+ assert_none("1234");
+
+ assert_some(0x1234, "00001234");
+ assert_some(0x1234, "000000001234");
+ assert_some(0x1234, "100000001234");
+ assert_some(0x12340000, "123400000000");
+ assert_some(0x5678, "000000000000000000005678");
+ assert_some(0x1234, "000012340000000000000000");
+ assert_some(0xffff, "00000000000000000000FFFF");
+ assert_some(0xffff, "00000000000000000000ffff");
+ }
+}
diff --git a/src/device/pro.rs b/src/device/pro.rs
new file mode 100644
index 0000000..0d5443e
--- /dev/null
+++ b/src/device/pro.rs
@@ -0,0 +1,93 @@
+// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
+// SPDX-License-Identifier: MIT
+
+use crate::device::{Device, Model, Status};
+use crate::error::Error;
+use crate::otp::GenerateOtp;
+use crate::util::get_command_result;
+
+/// A Nitrokey Pro device without user or admin authentication.
+///
+/// Use the [`connect`][] method to obtain an instance wrapper or the [`connect_pro`] method 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::Error;
+///
+/// fn perform_user_task<'a>(device: &User<'a, Pro<'a>>) {}
+/// fn perform_other_task(device: &Pro) {}
+///
+/// # fn try_main() -> Result<(), Error> {
+/// let mut manager = nitrokey::take()?;
+/// let device = manager.connect_pro()?;
+/// let device = match device.authenticate_user("123456") {
+/// Ok(user) => {
+/// perform_user_task(&user);
+/// user.device()
+/// },
+/// Err((device, err)) => {
+/// eprintln!("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`]: struct.Manager.html#method.connect
+/// [`connect_pro`]: struct.Manager.html#method.connect_pro
+#[derive(Debug)]
+pub struct Pro<'a> {
+ manager: Option<&'a mut crate::Manager>,
+}
+
+impl<'a> Pro<'a> {
+ pub(crate) fn new(manager: &'a mut crate::Manager) -> Pro<'a> {
+ Pro {
+ manager: Some(manager),
+ }
+ }
+}
+
+impl<'a> Drop for Pro<'a> {
+ fn drop(&mut self) {
+ unsafe {
+ nitrokey_sys::NK_logout();
+ }
+ }
+}
+
+impl<'a> Device<'a> for Pro<'a> {
+ fn into_manager(mut self) -> &'a mut crate::Manager {
+ self.manager.take().unwrap()
+ }
+
+ fn get_model(&self) -> Model {
+ Model::Pro
+ }
+
+ fn get_status(&self) -> Result<Status, Error> {
+ let mut raw_status = nitrokey_sys::NK_status {
+ firmware_version_major: 0,
+ firmware_version_minor: 0,
+ serial_number_smart_card: 0,
+ config_numlock: 0,
+ config_capslock: 0,
+ config_scrolllock: 0,
+ otp_user_password: false,
+ };
+ get_command_result(unsafe { nitrokey_sys::NK_get_status(&mut raw_status) })?;
+ Ok(raw_status.into())
+ }
+}
+
+impl<'a> GenerateOtp for Pro<'a> {}
diff --git a/src/device.rs b/src/device/storage.rs
index 758d4c1..a18d94f 100644
--- a/src/device.rs
+++ b/src/device/storage.rs
@@ -1,165 +1,14 @@
-// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
+// Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT
+use std::convert::TryFrom as _;
use std::fmt;
+use std::ops;
-use libc;
-use nitrokey_sys;
-
-use crate::auth::Authenticate;
-use crate::config::{Config, RawConfig};
-use crate::error::{CommunicationError, Error};
+use crate::device::{Device, FirmwareVersion, Model, SerialNumber, Status};
+use crate::error::{CommandError, Error};
use crate::otp::GenerateOtp;
-use crate::pws::GetPasswordSafe;
-use crate::util::{
- get_command_result, get_cstring, get_last_error, result_from_string, result_or_error,
-};
-
-/// 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 {
- f.write_str(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 {
- f.write_str(match *self {
- VolumeMode::ReadOnly => "read-only",
- VolumeMode::ReadWrite => "read-write",
- })
- }
-}
-
-/// A wrapper for a Nitrokey device of unknown type.
-///
-/// Use the [`connect`][] method 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::Error;
-///
-/// fn perform_user_task<'a>(device: &User<'a, DeviceWrapper<'a>>) {}
-/// fn perform_other_task(device: &DeviceWrapper) {}
-///
-/// # fn try_main() -> Result<(), Error> {
-/// let mut manager = nitrokey::take()?;
-/// let device = manager.connect()?;
-/// let device = match device.authenticate_user("123456") {
-/// Ok(user) => {
-/// perform_user_task(&user);
-/// user.device()
-/// },
-/// Err((device, err)) => {
-/// eprintln!("Could not authenticate as user: {}", err);
-/// device
-/// },
-/// };
-/// perform_other_task(&device);
-/// # Ok(())
-/// # }
-/// ```
-///
-/// Device-specific commands:
-///
-/// ```no_run
-/// use nitrokey::{DeviceWrapper, Storage};
-/// # use nitrokey::Error;
-///
-/// fn perform_common_task(device: &DeviceWrapper) {}
-/// fn perform_storage_task(device: &Storage) {}
-///
-/// # fn try_main() -> Result<(), Error> {
-/// let mut manager = nitrokey::take()?;
-/// let device = manager.connect()?;
-/// perform_common_task(&device);
-/// match device {
-/// DeviceWrapper::Storage(storage) => perform_storage_task(&storage),
-/// _ => (),
-/// };
-/// # Ok(())
-/// # }
-/// ```
-///
-/// [`connect`]: struct.Manager.html#method.connect
-#[derive(Debug)]
-pub enum DeviceWrapper<'a> {
- /// A Nitrokey Storage device.
- Storage(Storage<'a>),
- /// A Nitrokey Pro device.
- Pro(Pro<'a>),
-}
-
-/// A Nitrokey Pro device without user or admin authentication.
-///
-/// Use the [`connect`][] method to obtain an instance wrapper or the [`connect_pro`] method 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::Error;
-///
-/// fn perform_user_task<'a>(device: &User<'a, Pro<'a>>) {}
-/// fn perform_other_task(device: &Pro) {}
-///
-/// # fn try_main() -> Result<(), Error> {
-/// let mut manager = nitrokey::take()?;
-/// let device = manager.connect_pro()?;
-/// let device = match device.authenticate_user("123456") {
-/// Ok(user) => {
-/// perform_user_task(&user);
-/// user.device()
-/// },
-/// Err((device, err)) => {
-/// eprintln!("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`]: struct.Manager.html#method.connect
-/// [`connect_pro`]: struct.Manager.html#method.connect_pro
-#[derive(Debug)]
-pub struct Pro<'a> {
- manager: Option<&'a mut crate::Manager>,
-}
+use crate::util::{get_command_result, get_cstring, get_last_error};
/// A Nitrokey Storage device without user or admin authentication.
///
@@ -205,6 +54,24 @@ pub struct Storage<'a> {
manager: Option<&'a mut crate::Manager>,
}
+/// 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 {
+ f.write_str(match *self {
+ VolumeMode::ReadOnly => "read-only",
+ VolumeMode::ReadWrite => "read-write",
+ })
+ }
+}
+
/// The status of a volume on a Nitrokey Storage device.
#[derive(Debug)]
pub struct VolumeStatus {
@@ -231,21 +98,6 @@ pub struct SdCardData {
pub manufacturer: u8,
}
-/// A firmware version for a Nitrokey device.
-#[derive(Clone, Copy, Debug, PartialEq)]
-pub struct FirmwareVersion {
- /// The major firmware version, e. g. 0 in v0.40.
- pub major: u8,
- /// The minor firmware version, e. g. 40 in v0.40.
- pub minor: u8,
-}
-
-impl fmt::Display for FirmwareVersion {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "v{}.{}", self.major, self.minor)
- }
-}
-
/// Production information for a Storage device.
#[derive(Debug)]
pub struct StorageProductionInfo {
@@ -289,503 +141,20 @@ pub struct StorageStatus {
pub stick_initialized: bool,
}
-/// A Nitrokey device.
+/// The progress of a background operation on the Nitrokey.
///
-/// This trait provides the commands that can be executed without authentication and that are
-/// present on all supported Nitrokey devices.
-pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt::Debug {
- /// Returns the [`Manager`][] instance that has been used to connect to this device.
- ///
- /// # Example
- ///
- /// ```
- /// use nitrokey::{Device, DeviceWrapper};
- ///
- /// fn do_something(device: DeviceWrapper) {
- /// // reconnect to any device
- /// let manager = device.into_manager();
- /// let device = manager.connect();
- /// // do something with the device
- /// // ...
- /// }
- ///
- /// # 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(())
- /// # }
- /// ```
- fn into_manager(self) -> &'a mut crate::Manager;
-
- /// Returns the model of the connected Nitrokey device.
- ///
- /// # Example
- ///
- /// ```no_run
- /// use nitrokey::Device;
- /// # use nitrokey::Error;
- ///
- /// # fn try_main() -> Result<(), Error> {
- /// let mut manager = nitrokey::take()?;
- /// let device = manager.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::Error;
- ///
- /// # fn try_main() -> Result<(), Error> {
- /// let mut manager = nitrokey::take()?;
- /// let device = manager.connect()?;
- /// match device.get_serial_number() {
- /// Ok(number) => println!("serial no: {}", number),
- /// Err(err) => eprintln!("Could not get serial number: {}", err),
- /// };
- /// # Ok(())
- /// # }
- /// ```
- fn get_serial_number(&self) -> Result<String, Error> {
- result_from_string(unsafe { 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::Error;
- ///
- /// # fn try_main() -> Result<(), Error> {
- /// let mut manager = nitrokey::take()?;
- /// let device = manager.connect()?;
- /// let count = device.get_user_retry_count();
- /// match device.get_user_retry_count() {
- /// Ok(count) => println!("{} remaining authentication attempts (user)", count),
- /// Err(err) => eprintln!("Could not get user retry count: {}", err),
- /// }
- /// # Ok(())
- /// # }
- /// ```
- fn get_user_retry_count(&self) -> Result<u8, Error> {
- result_or_error(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::Error;
- ///
- /// # fn try_main() -> Result<(), Error> {
- /// let mut manager = nitrokey::take()?;
- /// let device = manager.connect()?;
- /// let count = device.get_admin_retry_count();
- /// match device.get_admin_retry_count() {
- /// Ok(count) => println!("{} remaining authentication attempts (admin)", count),
- /// Err(err) => eprintln!("Could not get admin retry count: {}", err),
- /// }
- /// # Ok(())
- /// # }
- /// ```
- fn get_admin_retry_count(&self) -> Result<u8, Error> {
- result_or_error(unsafe { nitrokey_sys::NK_get_admin_retry_count() })
- }
-
- /// Returns the firmware version.
- ///
- /// # Example
- ///
- /// ```no_run
- /// use nitrokey::Device;
- /// # use nitrokey::Error;
- ///
- /// # fn try_main() -> Result<(), Error> {
- /// let mut manager = nitrokey::take()?;
- /// let device = manager.connect()?;
- /// match device.get_firmware_version() {
- /// Ok(version) => println!("Firmware version: {}", version),
- /// Err(err) => eprintln!("Could not access firmware version: {}", err),
- /// };
- /// # Ok(())
- /// # }
- /// ```
- fn get_firmware_version(&self) -> Result<FirmwareVersion, Error> {
- let major = result_or_error(unsafe { nitrokey_sys::NK_get_major_firmware_version() })?;
- let minor = result_or_error(unsafe { nitrokey_sys::NK_get_minor_firmware_version() })?;
- Ok(FirmwareVersion { major, minor })
- }
-
- /// Returns the current configuration of the Nitrokey device.
- ///
- /// # Example
- ///
- /// ```no_run
- /// use nitrokey::Device;
- /// # use nitrokey::Error;
- ///
- /// # fn try_main() -> Result<(), Error> {
- /// let mut manager = nitrokey::take()?;
- /// let device = manager.connect()?;
- /// let config = device.get_config()?;
- /// println!("numlock binding: {:?}", config.numlock);
- /// println!("capslock binding: {:?}", config.capslock);
- /// println!("scrollock binding: {:?}", config.scrollock);
- /// println!("require password for OTP: {:?}", config.user_password);
- /// # Ok(())
- /// # }
- /// ```
- fn get_config(&self) -> Result<Config, Error> {
- let config_ptr = unsafe { 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 = unsafe { RawConfig::from(*config_array_ptr) };
- unsafe { libc::free(config_ptr as *mut libc::c_void) };
- 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::Error;
- ///
- /// # fn try_main() -> Result<(), Error> {
- /// let mut manager = nitrokey::take()?;
- /// let mut device = manager.connect()?;
- /// match device.change_admin_pin("12345678", "12345679") {
- /// Ok(()) => println!("Updated admin PIN."),
- /// Err(err) => eprintln!("Failed to update admin PIN: {}", err),
- /// };
- /// # Ok(())
- /// # }
- /// ```
- ///
- /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
- /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
- fn change_admin_pin(&mut self, current: &str, new: &str) -> Result<(), Error> {
- let current_string = get_cstring(current)?;
- let new_string = get_cstring(new)?;
- get_command_result(unsafe {
- 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::Error;
- ///
- /// # fn try_main() -> Result<(), Error> {
- /// let mut manager = nitrokey::take()?;
- /// let mut device = manager.connect()?;
- /// match device.change_user_pin("123456", "123457") {
- /// Ok(()) => println!("Updated admin PIN."),
- /// Err(err) => eprintln!("Failed to update admin PIN: {}", err),
- /// };
- /// # Ok(())
- /// # }
- /// ```
- ///
- /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
- /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
- fn change_user_pin(&mut self, current: &str, new: &str) -> Result<(), Error> {
- let current_string = get_cstring(current)?;
- let new_string = get_cstring(new)?;
- get_command_result(unsafe {
- 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::Error;
- ///
- /// # fn try_main() -> Result<(), Error> {
- /// let mut manager = nitrokey::take()?;
- /// let mut device = manager.connect()?;
- /// match device.unlock_user_pin("12345678", "123456") {
- /// Ok(()) => println!("Unlocked user PIN."),
- /// Err(err) => eprintln!("Failed to unlock user PIN: {}", err),
- /// };
- /// # Ok(())
- /// # }
- /// ```
- ///
- /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
- /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
- fn unlock_user_pin(&mut self, admin_pin: &str, user_pin: &str) -> Result<(), Error> {
- let admin_pin_string = get_cstring(admin_pin)?;
- let user_pin_string = get_cstring(user_pin)?;
- get_command_result(unsafe {
- 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::Error;
- ///
- /// # fn try_main() -> Result<(), Error> {
- /// let mut manager = nitrokey::take()?;
- /// let mut device = manager.connect()?;
- /// match device.lock() {
- /// Ok(()) => println!("Locked the Nitrokey device."),
- /// Err(err) => eprintln!("Could not lock the Nitrokey device: {}", err),
- /// };
- /// # Ok(())
- /// # }
- /// ```
- fn lock(&mut self) -> Result<(), Error> {
- get_command_result(unsafe { 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::Error;
- ///
- /// # fn try_main() -> Result<(), Error> {
- /// let mut manager = nitrokey::take()?;
- /// let mut device = manager.connect()?;
- /// match device.factory_reset("12345678") {
- /// Ok(()) => println!("Performed a factory reset."),
- /// Err(err) => eprintln!("Could not perform a factory reset: {}", err),
- /// };
- /// # Ok(())
- /// # }
- /// ```
- ///
- /// [`build_aes_key`]: #method.build_aes_key
- fn factory_reset(&mut self, admin_pin: &str) -> Result<(), Error> {
- let admin_pin_string = get_cstring(admin_pin)?;
- get_command_result(unsafe { 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::Error;
- ///
- /// # fn try_main() -> Result<(), Error> {
- /// let mut manager = nitrokey::take()?;
- /// let mut device = manager.connect()?;
- /// match device.build_aes_key("12345678") {
- /// Ok(()) => println!("New AES keys have been built."),
- /// Err(err) => eprintln!("Could not build new AES keys: {}", err),
- /// };
- /// # Ok(())
- /// # }
- /// ```
- ///
- /// [`factory_reset`]: #method.factory_reset
- fn build_aes_key(&mut self, admin_pin: &str) -> Result<(), Error> {
- let admin_pin_string = get_cstring(admin_pin)?;
- get_command_result(unsafe { nitrokey_sys::NK_build_aes_key(admin_pin_string.as_ptr()) })
- }
-}
-
-fn get_connected_model() -> Option<Model> {
- match unsafe { nitrokey_sys::NK_get_device_model() } {
- nitrokey_sys::NK_device_model_NK_PRO => Some(Model::Pro),
- nitrokey_sys::NK_device_model_NK_STORAGE => Some(Model::Storage),
- _ => None,
- }
-}
-
-pub(crate) fn create_device_wrapper(
- manager: &mut crate::Manager,
- model: Model,
-) -> DeviceWrapper<'_> {
- match model {
- Model::Pro => Pro::new(manager).into(),
- Model::Storage => Storage::new(manager).into(),
- }
-}
-
-pub(crate) fn get_connected_device(
- manager: &mut crate::Manager,
-) -> Result<DeviceWrapper<'_>, Error> {
- match get_connected_model() {
- Some(model) => Ok(create_device_wrapper(manager, model)),
- None => Err(CommunicationError::NotConnected.into()),
- }
-}
-
-pub(crate) fn connect_enum(model: Model) -> bool {
- let model = match model {
- Model::Storage => nitrokey_sys::NK_device_model_NK_STORAGE,
- Model::Pro => nitrokey_sys::NK_device_model_NK_PRO,
- };
- unsafe { nitrokey_sys::NK_login_enum(model) == 1 }
-}
-
-impl<'a> DeviceWrapper<'a> {
- fn device(&self) -> &dyn Device<'a> {
- match *self {
- DeviceWrapper::Storage(ref storage) => storage,
- DeviceWrapper::Pro(ref pro) => pro,
- }
- }
-
- fn device_mut(&mut self) -> &mut dyn Device<'a> {
- match *self {
- DeviceWrapper::Storage(ref mut storage) => storage,
- DeviceWrapper::Pro(ref mut pro) => pro,
- }
- }
-}
-
-impl<'a> From<Pro<'a>> for DeviceWrapper<'a> {
- fn from(device: Pro<'a>) -> Self {
- DeviceWrapper::Pro(device)
- }
-}
-
-impl<'a> From<Storage<'a>> for DeviceWrapper<'a> {
- fn from(device: Storage<'a>) -> Self {
- DeviceWrapper::Storage(device)
- }
-}
-
-impl<'a> GenerateOtp for DeviceWrapper<'a> {
- fn get_hotp_slot_name(&self, slot: u8) -> Result<String, Error> {
- self.device().get_hotp_slot_name(slot)
- }
-
- fn get_totp_slot_name(&self, slot: u8) -> Result<String, Error> {
- self.device().get_totp_slot_name(slot)
- }
-
- fn get_hotp_code(&mut self, slot: u8) -> Result<String, Error> {
- self.device_mut().get_hotp_code(slot)
- }
-
- fn get_totp_code(&self, slot: u8) -> Result<String, Error> {
- self.device().get_totp_code(slot)
- }
-}
-
-impl<'a> Device<'a> for DeviceWrapper<'a> {
- fn into_manager(self) -> &'a mut crate::Manager {
- match self {
- DeviceWrapper::Pro(dev) => dev.into_manager(),
- DeviceWrapper::Storage(dev) => dev.into_manager(),
- }
- }
-
- fn get_model(&self) -> Model {
- match *self {
- DeviceWrapper::Pro(_) => Model::Pro,
- DeviceWrapper::Storage(_) => Model::Storage,
- }
- }
-}
-
-impl<'a> Pro<'a> {
- pub(crate) fn new(manager: &'a mut crate::Manager) -> Pro<'a> {
- Pro {
- manager: Some(manager),
- }
- }
-}
-
-impl<'a> Drop for Pro<'a> {
- fn drop(&mut self) {
- unsafe {
- nitrokey_sys::NK_logout();
- }
- }
-}
-
-impl<'a> Device<'a> for Pro<'a> {
- fn into_manager(mut self) -> &'a mut crate::Manager {
- self.manager.take().unwrap()
- }
-
- fn get_model(&self) -> Model {
- Model::Pro
- }
+/// Some commands may start a background operation during which no other commands can be executed.
+/// This enum stores the status of a background operation: Ongoing with a relative progress (up to
+/// 100), or idle, i. e. no background operation has been started or the last one has been
+/// finished.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum OperationStatus {
+ /// A background operation with its progress value (less than or equal to 100).
+ Ongoing(u8),
+ /// No backgrund operation.
+ Idle,
}
-impl<'a> GenerateOtp for Pro<'a> {}
-
impl<'a> Storage<'a> {
pub(crate) fn new(manager: &'a mut crate::Manager) -> Storage<'a> {
Storage {
@@ -1170,7 +539,7 @@ impl<'a> Storage<'a> {
/// # fn try_main() -> Result<(), Error> {
/// let mut manager = nitrokey::take()?;
/// let device = manager.connect_storage()?;
- /// match device.get_status() {
+ /// match device.get_storage_status() {
/// Ok(status) => {
/// println!("SD card ID: {:#x}", status.serial_number_sd_card);
/// },
@@ -1179,7 +548,7 @@ impl<'a> Storage<'a> {
/// # Ok(())
/// # }
/// ```
- pub fn get_status(&self) -> Result<StorageStatus, Error> {
+ pub fn get_storage_status(&self) -> Result<StorageStatus, Error> {
let mut raw_status = nitrokey_sys::NK_storage_status {
unencrypted_volume_read_only: false,
unencrypted_volume_active: false,
@@ -1280,11 +649,121 @@ impl<'a> Storage<'a> {
})
}
+ /// Returns a range of the SD card that has not been used to during this power cycle.
+ ///
+ /// The Nitrokey Storage tracks read and write access to the SD card during a power cycle.
+ /// This method returns a range of the SD card that has not been accessed during this power
+ /// cycle. The range is relative to the total size of the SD card, so both values are less
+ /// than or equal to 100. This can be used as a guideline when creating a hidden volume.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// let mut manager = nitrokey::take()?;
+ /// let storage = manager.connect_storage()?;
+ /// let usage = storage.get_sd_card_usage()?;
+ /// println!("SD card usage: {}..{}", usage.start, usage.end);
+ /// # Ok::<(), nitrokey::Error>(())
+ /// ```
+ pub fn get_sd_card_usage(&self) -> Result<ops::Range<u8>, Error> {
+ let mut usage_data = nitrokey_sys::NK_SD_usage_data {
+ write_level_min: 0,
+ write_level_max: 0,
+ };
+ let result = unsafe { nitrokey_sys::NK_get_SD_usage_data(&mut usage_data) };
+ match get_command_result(result) {
+ Ok(_) => {
+ if usage_data.write_level_min > usage_data.write_level_max
+ || usage_data.write_level_max > 100
+ {
+ Err(Error::UnexpectedError("Invalid write levels".to_owned()))
+ } else {
+ Ok(ops::Range {
+ start: usage_data.write_level_min,
+ end: usage_data.write_level_max,
+ })
+ }
+ }
+ Err(err) => Err(err),
+ }
+ }
+
/// Blinks the red and green LED alternatively and infinitely until the device is reconnected.
pub fn wink(&mut self) -> Result<(), Error> {
get_command_result(unsafe { nitrokey_sys::NK_wink() })
}
+ /// Returns the status of an ongoing background operation on the Nitrokey Storage.
+ ///
+ /// Some commands may start a background operation during which no other commands can be
+ /// executed. This method can be used to check whether such an operation is ongoing.
+ ///
+ /// Currently, this is only used by the [`fill_sd_card`][] method.
+ ///
+ /// [`fill_sd_card`]: #method.fill_sd_card
+ pub fn get_operation_status(&self) -> Result<OperationStatus, Error> {
+ let status = unsafe { nitrokey_sys::NK_get_progress_bar_value() };
+ match status {
+ 0..=100 => u8::try_from(status)
+ .map(OperationStatus::Ongoing)
+ .map_err(|_| {
+ Error::UnexpectedError("Cannot create u8 from operation status".to_owned())
+ }),
+ -1 => Ok(OperationStatus::Idle),
+ -2 => Err(get_last_error()),
+ _ => Err(Error::UnexpectedError(
+ "Invalid operation status".to_owned(),
+ )),
+ }
+ }
+
+ /// Overwrites the SD card with random data.
+ ///
+ /// Ths method starts a background operation that overwrites the SD card with random data.
+ /// While this operation is ongoing, no other commands can be executed. Use the
+ /// [`get_operation_status`][] function to check the progress of the operation.
+ ///
+ /// # Errors
+ ///
+ /// - [`InvalidString`][] if one of the provided passwords contains a null byte
+ /// - [`WrongPassword`][] if the admin password is wrong
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::OperationStatus;
+ ///
+ /// let mut manager = nitrokey::take()?;
+ /// let mut storage = manager.connect_storage()?;
+ /// storage.fill_sd_card("12345678")?;
+ /// loop {
+ /// match storage.get_operation_status()? {
+ /// OperationStatus::Ongoing(progress) => println!("{}/100", progress),
+ /// OperationStatus::Idle => {
+ /// println!("Done!");
+ /// break;
+ /// }
+ /// }
+ /// }
+ /// # Ok::<(), nitrokey::Error>(())
+ /// ```
+ ///
+ /// [`get_operation_status`]: #method.get_operation_status
+ /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
+ /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
+ pub fn fill_sd_card(&mut self, admin_pin: &str) -> Result<(), Error> {
+ let admin_pin_string = get_cstring(admin_pin)?;
+ get_command_result(unsafe {
+ nitrokey_sys::NK_fill_SD_card_with_random_data(admin_pin_string.as_ptr())
+ })
+ .or_else(|err| match err {
+ // libnitrokey’s C API returns a LongOperationInProgressException with the same error
+ // code as the WrongCrc command error, so we cannot distinguish them.
+ Error::CommandError(CommandError::WrongCrc) => Ok(()),
+ err => Err(err),
+ })
+ }
+
/// Exports the firmware to the unencrypted volume.
///
/// This command requires the admin PIN. The unencrypted volume must be in read-write mode
@@ -1323,6 +802,33 @@ impl<'a> Device<'a> for Storage<'a> {
fn get_model(&self) -> Model {
Model::Storage
}
+
+ fn get_status(&self) -> Result<Status, Error> {
+ // Currently, the GET_STATUS command does not report the correct firmware version and
+ // serial number on the Nitrokey Storage, see [0]. Until this is fixed in libnitrokey, we
+ // have to manually execute the GET_DEVICE_STATUS command (get_storage_status) and complete
+ // the missing data, see [1].
+ // [0] https://github.com/Nitrokey/nitrokey-storage-firmware/issues/96
+ // [1] https://github.com/Nitrokey/libnitrokey/issues/166
+
+ let mut raw_status = nitrokey_sys::NK_status {
+ firmware_version_major: 0,
+ firmware_version_minor: 0,
+ serial_number_smart_card: 0,
+ config_numlock: 0,
+ config_capslock: 0,
+ config_scrolllock: 0,
+ otp_user_password: false,
+ };
+ get_command_result(unsafe { nitrokey_sys::NK_get_status(&mut raw_status) })?;
+ let mut status = Status::from(raw_status);
+
+ let storage_status = self.get_storage_status()?;
+ status.firmware_version = storage_status.firmware_version;
+ status.serial_number = SerialNumber::new(storage_status.serial_number_smart_card);
+
+ Ok(status)
+ }
}
impl<'a> GenerateOtp for Storage<'a> {}
diff --git a/src/device/wrapper.rs b/src/device/wrapper.rs
new file mode 100644
index 0000000..942a905
--- /dev/null
+++ b/src/device/wrapper.rs
@@ -0,0 +1,142 @@
+// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
+// SPDX-License-Identifier: MIT
+
+use crate::device::{Device, Model, Pro, Status, Storage};
+use crate::error::Error;
+use crate::otp::GenerateOtp;
+
+/// A wrapper for a Nitrokey device of unknown type.
+///
+/// Use the [`connect`][] method 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::Error;
+///
+/// fn perform_user_task<'a>(device: &User<'a, DeviceWrapper<'a>>) {}
+/// fn perform_other_task(device: &DeviceWrapper) {}
+///
+/// # fn try_main() -> Result<(), Error> {
+/// let mut manager = nitrokey::take()?;
+/// let device = manager.connect()?;
+/// let device = match device.authenticate_user("123456") {
+/// Ok(user) => {
+/// perform_user_task(&user);
+/// user.device()
+/// },
+/// Err((device, err)) => {
+/// eprintln!("Could not authenticate as user: {}", err);
+/// device
+/// },
+/// };
+/// perform_other_task(&device);
+/// # Ok(())
+/// # }
+/// ```
+///
+/// Device-specific commands:
+///
+/// ```no_run
+/// use nitrokey::{DeviceWrapper, Storage};
+/// # use nitrokey::Error;
+///
+/// fn perform_common_task(device: &DeviceWrapper) {}
+/// fn perform_storage_task(device: &Storage) {}
+///
+/// # fn try_main() -> Result<(), Error> {
+/// let mut manager = nitrokey::take()?;
+/// let device = manager.connect()?;
+/// perform_common_task(&device);
+/// match device {
+/// DeviceWrapper::Storage(storage) => perform_storage_task(&storage),
+/// _ => (),
+/// };
+/// # Ok(())
+/// # }
+/// ```
+///
+/// [`connect`]: struct.Manager.html#method.connect
+#[derive(Debug)]
+#[non_exhaustive]
+pub enum DeviceWrapper<'a> {
+ /// A Nitrokey Storage device.
+ Storage(Storage<'a>),
+ /// A Nitrokey Pro device.
+ Pro(Pro<'a>),
+}
+
+impl<'a> DeviceWrapper<'a> {
+ fn device(&self) -> &dyn Device<'a> {
+ match *self {
+ DeviceWrapper::Storage(ref storage) => storage,
+ DeviceWrapper::Pro(ref pro) => pro,
+ }
+ }
+
+ fn device_mut(&mut self) -> &mut dyn Device<'a> {
+ match *self {
+ DeviceWrapper::Storage(ref mut storage) => storage,
+ DeviceWrapper::Pro(ref mut pro) => pro,
+ }
+ }
+}
+
+impl<'a> From<Pro<'a>> for DeviceWrapper<'a> {
+ fn from(device: Pro<'a>) -> Self {
+ DeviceWrapper::Pro(device)
+ }
+}
+
+impl<'a> From<Storage<'a>> for DeviceWrapper<'a> {
+ fn from(device: Storage<'a>) -> Self {
+ DeviceWrapper::Storage(device)
+ }
+}
+
+impl<'a> GenerateOtp for DeviceWrapper<'a> {
+ fn get_hotp_slot_name(&self, slot: u8) -> Result<String, Error> {
+ self.device().get_hotp_slot_name(slot)
+ }
+
+ fn get_totp_slot_name(&self, slot: u8) -> Result<String, Error> {
+ self.device().get_totp_slot_name(slot)
+ }
+
+ fn get_hotp_code(&mut self, slot: u8) -> Result<String, Error> {
+ self.device_mut().get_hotp_code(slot)
+ }
+
+ fn get_totp_code(&self, slot: u8) -> Result<String, Error> {
+ self.device().get_totp_code(slot)
+ }
+}
+
+impl<'a> Device<'a> for DeviceWrapper<'a> {
+ fn into_manager(self) -> &'a mut crate::Manager {
+ match self {
+ DeviceWrapper::Pro(dev) => dev.into_manager(),
+ DeviceWrapper::Storage(dev) => dev.into_manager(),
+ }
+ }
+
+ fn get_model(&self) -> Model {
+ match *self {
+ DeviceWrapper::Pro(_) => Model::Pro,
+ DeviceWrapper::Storage(_) => Model::Storage,
+ }
+ }
+
+ fn get_status(&self) -> Result<Status, Error> {
+ match self {
+ DeviceWrapper::Pro(dev) => dev.get_status(),
+ DeviceWrapper::Storage(dev) => dev.get_status(),
+ }
+ }
+}
diff --git a/src/error.rs b/src/error.rs
index 9e6adc0..f1e91c3 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -11,6 +11,7 @@ use crate::device;
/// An error returned by the nitrokey crate.
#[derive(Debug)]
+#[non_exhaustive]
pub enum Error {
/// An error reported by the Nitrokey device in the response packet.
CommandError(CommandError),
@@ -21,13 +22,13 @@ pub enum Error {
/// A library usage error.
LibraryError(LibraryError),
/// An error that occurred due to a poisoned lock.
- PoisonError(sync::PoisonError<sync::MutexGuard<'static, crate::Manager>>),
- /// An error that occurred during random number generation.
- RandError(Box<dyn error::Error>),
+ PoisonError,
/// An error that is caused by an unexpected value returned by libnitrokey.
- UnexpectedError,
+ UnexpectedError(String),
/// An unknown error returned by libnitrokey.
UnknownError(i64),
+ /// An error caused by a Nitrokey model that is not supported by this crate.
+ UnsupportedModelError,
/// An error occurred when interpreting a UTF-8 string.
Utf8Error(str::Utf8Error),
}
@@ -70,14 +71,14 @@ impl From<str::Utf8Error> for Error {
}
}
-impl From<sync::PoisonError<sync::MutexGuard<'static, crate::Manager>>> for Error {
- fn from(error: sync::PoisonError<sync::MutexGuard<'static, crate::Manager>>) -> Self {
- Error::PoisonError(error)
+impl<T> From<sync::PoisonError<T>> for Error {
+ fn from(_error: sync::PoisonError<T>) -> Self {
+ Error::PoisonError
}
}
-impl From<sync::TryLockError<sync::MutexGuard<'static, crate::Manager>>> for Error {
- fn from(error: sync::TryLockError<sync::MutexGuard<'static, crate::Manager>>) -> Self {
+impl<T> From<sync::TryLockError<T>> for Error {
+ fn from(error: sync::TryLockError<T>) -> Self {
match error {
sync::TryLockError::Poisoned(err) => err.into(),
sync::TryLockError::WouldBlock => Error::ConcurrentAccessError,
@@ -91,21 +92,7 @@ impl<'a, T: device::Device<'a>> From<(T, Error)> for Error {
}
}
-impl error::Error for Error {
- fn source(&self) -> Option<&(dyn error::Error + 'static)> {
- match *self {
- Error::CommandError(ref err) => Some(err),
- Error::CommunicationError(ref err) => Some(err),
- Error::ConcurrentAccessError => None,
- Error::LibraryError(ref err) => Some(err),
- Error::PoisonError(ref err) => Some(err),
- Error::RandError(ref err) => Some(err.as_ref()),
- Error::UnexpectedError => None,
- Error::UnknownError(_) => None,
- Error::Utf8Error(ref err) => Some(err),
- }
- }
-}
+impl error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -114,10 +101,10 @@ impl fmt::Display for Error {
Error::CommunicationError(ref err) => write!(f, "Communication error: {}", err),
Error::ConcurrentAccessError => write!(f, "Internal error: concurrent access"),
Error::LibraryError(ref err) => write!(f, "Library error: {}", err),
- Error::PoisonError(_) => write!(f, "Internal error: poisoned lock"),
- Error::RandError(ref err) => write!(f, "RNG error: {}", err),
- Error::UnexpectedError => write!(f, "An unexpected error occurred"),
+ Error::PoisonError => write!(f, "Internal error: poisoned lock"),
+ Error::UnexpectedError(ref s) => write!(f, "An unexpected error occurred: {}", s),
Error::UnknownError(ref err) => write!(f, "Unknown error: {}", err),
+ Error::UnsupportedModelError => write!(f, "Unsupported Nitrokey model"),
Error::Utf8Error(ref err) => write!(f, "UTF-8 error: {}", err),
}
}
@@ -125,6 +112,7 @@ impl fmt::Display for Error {
/// An error reported by the Nitrokey device in the response packet.
#[derive(Clone, Copy, Debug, PartialEq)]
+#[non_exhaustive]
pub enum CommandError {
/// A packet with a wrong checksum has been sent or received.
WrongCrc,
@@ -191,6 +179,7 @@ impl fmt::Display for CommandError {
/// A device communication error.
#[derive(Clone, Copy, Debug, PartialEq)]
+#[non_exhaustive]
pub enum CommunicationError {
/// Could not connect to a Nitrokey device.
NotConnected,
@@ -229,6 +218,7 @@ impl fmt::Display for CommunicationError {
/// A library usage error.
#[derive(Clone, Copy, Debug, PartialEq)]
+#[non_exhaustive]
pub enum LibraryError {
/// A supplied string exceeded a length limit.
StringTooLong,
@@ -267,3 +257,22 @@ impl fmt::Display for LibraryError {
})
}
}
+
+// build our own static assertion that Error implements error::Error, Send, Sync, 'static
+
+struct Helper<T>(T);
+
+trait Assert {
+ fn assert() -> bool;
+}
+
+impl<T: error::Error + Send + Sync + 'static> Assert for Helper<T> {
+ fn assert() -> bool {
+ true
+ }
+}
+
+#[allow(unused)]
+fn assert_error_impl() {
+ let _ = Helper::<Error>::assert();
+}
diff --git a/src/lib.rs b/src/lib.rs
index a4402c5..1f1ae27 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -16,6 +16,10 @@
//! [`connect_model`][], [`connect_pro`][] or [`connect_storage`][] to connect to a specific
//! device.
//!
+//! To get a list of all connected Nitrokey devices, use the [`list_devices`][] function. You can
+//! then connect to one of the connected devices using the [`connect_path`][] function of the
+//! `Manager` struct.
+//!
//! You can call [`authenticate_user`][] or [`authenticate_admin`][] to get an authenticated device
//! that can perform operations that require authentication. You can use [`device`][] to go back
//! to the unauthenticated device.
@@ -25,6 +29,15 @@
//! passwords – [`get_hotp_code`][] and [`get_totp_code`][]. Depending on the stick configuration,
//! these operations are available without authentication or with user authentication.
//!
+//! # Background operations
+//!
+//! Some commands may start background operations. During such an operation, every new command
+//! will cause a [`WrongCrc`][] error. To check whether a background operation is currently
+//! running, use the [`get_operation_status`][] method.
+//!
+//! Background operations are only available on the Nitrokey Storage. Currently,
+//! [`fill_sd_card`][] is the only command that triggers a background operation.
+//!
//! # Examples
//!
//! Connect to any Nitrokey and print its serial number:
@@ -86,8 +99,12 @@
//! [`take`]: fn.take.html
//! [`connect`]: struct.Manager.html#method.connect
//! [`connect_model`]: struct.Manager.html#method.connect_model
+//! [`connect_path`]: struct.Manager.html#method.connect_path
//! [`connect_pro`]: struct.Manager.html#method.connect_pro
//! [`connect_storage`]: struct.Manager.html#method.connect_storage
+//! [`fill_sd_card`]: struct.Storage.html#method.fill_sd_card
+//! [`get_operation_status`]: struct.Storage.html#method.get_operation_status
+//! [`list_devices`]: fn.list_devices.html
//! [`manager`]: trait.Device.html#method.manager
//! [`device`]: struct.User.html#method.device
//! [`get_hotp_code`]: trait.GenerateOtp.html#method.get_hotp_code
@@ -95,6 +112,7 @@
//! [`Admin`]: struct.Admin.html
//! [`DeviceWrapper`]: enum.DeviceWrapper.html
//! [`User`]: struct.User.html
+//! [`WrongCrc`]: enum.CommandError.html#variant.WrongCrc
#![warn(missing_docs, rust_2018_compatibility, rust_2018_idioms, unused)]
@@ -109,23 +127,25 @@ mod otp;
mod pws;
mod util;
+use std::convert::TryInto as _;
use std::fmt;
use std::marker;
+use std::ptr::NonNull;
use std::sync;
-use nitrokey_sys;
-
pub use crate::auth::{Admin, Authenticate, User};
pub use crate::config::Config;
pub use crate::device::{
- Device, DeviceWrapper, Model, Pro, SdCardData, Storage, StorageProductionInfo, StorageStatus,
- VolumeMode, VolumeStatus,
+ Device, DeviceInfo, DeviceWrapper, FirmwareVersion, 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::LogLevel;
+use crate::util::{get_cstring, get_last_result};
+
/// The default admin PIN for all Nitrokey devices.
pub const DEFAULT_ADMIN_PIN: &str = "12345678";
/// The default user PIN for all Nitrokey devices.
@@ -235,6 +255,7 @@ impl Manager {
/// # Errors
///
/// - [`NotConnected`][] if no Nitrokey device is connected
+ /// - [`UnsupportedModelError`][] if the Nitrokey device is not supported by this crate
///
/// # Example
///
@@ -243,17 +264,16 @@ impl Manager {
///
/// fn do_something(device: DeviceWrapper) {}
///
- /// # fn main() -> Result<(), nitrokey::Error> {
/// let mut manager = nitrokey::take()?;
/// match manager.connect() {
/// Ok(device) => do_something(device),
/// Err(err) => println!("Could not connect to a Nitrokey: {}", err),
/// }
- /// # Ok(())
- /// # }
+ /// # 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)
@@ -276,13 +296,11 @@ impl Manager {
///
/// 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(())
- /// # }
+ /// # Ok::<(), nitrokey::Error>(())
/// ```
///
/// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
@@ -294,6 +312,49 @@ impl Manager {
}
}
+ /// Connects to a Nitrokey device at the given USB path.
+ ///
+ /// To get a list of all connected Nitrokey devices, use the [`list_devices`][] function. The
+ /// [`DeviceInfo`][] structs returned by that function contain the USB path in the `path`
+ /// field.
+ ///
+ /// # Errors
+ ///
+ /// - [`InvalidString`][] if the USB path contains a null byte
+ /// - [`NotConnected`][] if no Nitrokey device can be found at the given USB path
+ /// - [`UnsupportedModelError`][] if the model of the Nitrokey device at the given USB path is
+ /// not supported by this crate
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use nitrokey::DeviceWrapper;
+ ///
+ /// fn use_device(device: DeviceWrapper) {}
+ ///
+ /// let mut manager = nitrokey::take()?;
+ /// let devices = nitrokey::list_devices()?;
+ /// for device in devices {
+ /// let device = manager.connect_path(device.path)?;
+ /// use_device(device);
+ /// }
+ /// # Ok::<(), nitrokey::Error>(())
+ /// ```
+ ///
+ /// [`list_devices`]: fn.list_devices.html
+ /// [`DeviceInfo`]: struct.DeviceInfo.html
+ /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
+ /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
+ /// [`UnsupportedModelError`]: enum.Error.html#variant.UnsupportedModelError
+ pub fn connect_path<S: Into<Vec<u8>>>(&mut self, path: S) -> Result<DeviceWrapper<'_>, Error> {
+ let path = get_cstring(path)?;
+ if unsafe { nitrokey_sys::NK_connect_with_path(path.as_ptr()) } == 1 {
+ device::get_connected_device(self)
+ } else {
+ Err(CommunicationError::NotConnected.into())
+ }
+ }
+
/// Connects to a Nitrokey Pro.
///
/// # Errors
@@ -307,13 +368,11 @@ impl Manager {
///
/// 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(())
- /// # }
+ /// # Ok::<(), nitrokey::Error>(())
/// ```
///
/// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
@@ -338,13 +397,11 @@ impl Manager {
///
/// 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(())
- /// # }
+ /// # Ok::<(), nitrokey::Error>(())
/// ```
///
/// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
@@ -413,15 +470,77 @@ pub fn take() -> Result<sync::MutexGuard<'static, Manager>, Error> {
/// [`ConcurrentAccessError`]: struct.Error.html#variant.ConcurrentAccessError
/// [`Manager`]: struct.Manager.html
pub fn force_take() -> Result<sync::MutexGuard<'static, Manager>, Error> {
- match take() {
- Ok(guard) => Ok(guard),
- Err(err) => match err {
- Error::PoisonError(err) => Ok(err.into_inner()),
- err => Err(err),
- },
+ 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`][]).
@@ -453,11 +572,9 @@ pub fn set_log_level(level: LogLevel) {
/// # Example
///
/// ```
-/// # fn main() -> Result<(), nitrokey::Error> {
/// let version = nitrokey::get_library_version()?;
/// println!("Using libnitrokey {}", version.git);
-/// # Ok(())
-/// # }
+/// # Ok::<(), nitrokey::Error>(())
/// ```
///
/// [`Utf8Error`]: enum.Error.html#variant.Utf8Error
diff --git a/src/otp.rs b/src/otp.rs
index 4667aff..7b47a3b 100644
--- a/src/otp.rs
+++ b/src/otp.rs
@@ -3,8 +3,6 @@
use std::ffi::CString;
-use nitrokey_sys;
-
use crate::error::Error;
use crate::util::{get_command_result, get_cstring, result_from_string};
@@ -349,8 +347,8 @@ pub struct OtpSlotData {
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.
+ /// If true, press the enter key after sending an OTP code using double-pressed Num Lock, Caps
+ /// Lock or Scroll Lock.
pub use_enter: bool,
/// Set the token ID, see [OATH Token Identifier Specification][tokspec], section “Class A”.
///
@@ -387,8 +385,8 @@ impl OtpSlotData {
}
}
- /// Enables pressing the enter key after sending an OTP code using double-pressed numlock,
- /// capslock or scrollock.
+ /// Enables pressing the enter key after sending an OTP code using double-pressed Num Lock,
+ /// Caps Lock or Scroll Lock.
pub fn use_enter(mut self) -> OtpSlotData {
self.use_enter = true;
self
diff --git a/src/pws.rs b/src/pws.rs
index 3398deb..93294c5 100644
--- a/src/pws.rs
+++ b/src/pws.rs
@@ -1,9 +1,6 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT
-use libc;
-use nitrokey_sys;
-
use crate::device::{Device, DeviceWrapper, Pro, Storage};
use crate::error::{CommandError, Error};
use crate::util::{get_command_result, get_cstring, get_last_error, result_from_string};
diff --git a/src/util.rs b/src/util.rs
index a5dd1e5..26942cf 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -5,8 +5,7 @@ use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
use libc::{c_void, free};
-use rand_core::RngCore;
-use rand_os::OsRng;
+use rand_core::{OsRng, RngCore};
use crate::error::{Error, LibraryError};
@@ -31,26 +30,38 @@ pub enum LogLevel {
DebugL2,
}
+pub fn str_from_ptr<'a>(ptr: *const c_char) -> Result<&'a str, Error> {
+ unsafe { CStr::from_ptr(ptr) }.to_str().map_err(Error::from)
+}
+
pub fn owned_str_from_ptr(ptr: *const c_char) -> Result<String, Error> {
- unsafe { CStr::from_ptr(ptr) }
- .to_str()
- .map(String::from)
- .map_err(Error::from)
+ str_from_ptr(ptr).map(ToOwned::to_owned)
}
-pub fn result_from_string(ptr: *const c_char) -> Result<String, Error> {
+pub fn run_with_string<R, F>(ptr: *const c_char, op: F) -> Result<R, Error>
+where
+ F: FnOnce(&str) -> Result<R, Error>,
+{
if ptr.is_null() {
- return Err(Error::UnexpectedError);
+ return Err(Error::UnexpectedError(
+ "libnitrokey returned a null pointer".to_owned(),
+ ));
}
- let s = owned_str_from_ptr(ptr)?;
+ let result = str_from_ptr(ptr).and_then(op);
unsafe { 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)
- }
+ result
+}
+
+pub fn result_from_string(ptr: *const c_char) -> Result<String, Error> {
+ run_with_string(ptr, |s| {
+ // 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.to_owned())
+ } else {
+ Ok(s.to_owned())
+ }
+ })
}
pub fn result_or_error<T>(value: T) -> Result<T, Error> {
@@ -70,20 +81,25 @@ pub fn get_last_result() -> Result<(), Error> {
}
pub fn get_last_error() -> Error {
- match get_last_result() {
- Ok(()) => Error::UnexpectedError,
- Err(err) => err,
- }
+ get_last_result().err().unwrap_or_else(|| {
+ Error::UnexpectedError("Expected an error, but command status is zero".to_owned())
+ })
}
-pub fn generate_password(length: usize) -> Result<Vec<u8>, Error> {
- let mut data = vec![0u8; length];
- OsRng.fill_bytes(&mut data[..]);
- Ok(data)
+pub fn generate_password(length: usize) -> Result<CString, Error> {
+ loop {
+ // Randomly generate a password until we get a string *without* null bytes. Otherwise
+ // the string would be cut off prematurely due to null-termination in C.
+ let mut data = vec![0u8; length];
+ OsRng.fill_bytes(&mut data[..]);
+ if let Ok(s) = CString::new(data) {
+ return Ok(s);
+ }
+ }
}
pub fn get_cstring<T: Into<Vec<u8>>>(s: T) -> Result<CString, Error> {
- CString::new(s).or_else(|_| Err(LibraryError::InvalidString.into()))
+ CString::new(s).map_err(|_| LibraryError::InvalidString.into())
}
impl Into<i32> for LogLevel {
diff --git a/tests/device.rs b/tests/device.rs
index e367558..12206e2 100644
--- a/tests/device.rs
+++ b/tests/device.rs
@@ -1,4 +1,4 @@
-// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
+// Copyright (C) 2018-2020 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT
mod util;
@@ -8,9 +8,9 @@ use std::process::Command;
use std::{thread, time};
use nitrokey::{
- Authenticate, CommandError, CommunicationError, Config, ConfigureOtp, Device, Error,
- GenerateOtp, GetPasswordSafe, LibraryError, OtpMode, OtpSlotData, Storage, VolumeMode,
- DEFAULT_ADMIN_PIN, DEFAULT_USER_PIN,
+ Authenticate, CommandError, CommunicationError, Config, ConfigureOtp, Device, DeviceInfo,
+ Error, GenerateOtp, GetPasswordSafe, LibraryError, OperationStatus, OtpMode, OtpSlotData,
+ SerialNumber, Storage, VolumeMode, DEFAULT_ADMIN_PIN, DEFAULT_USER_PIN,
};
use nitrokey_test::test as test_device;
@@ -32,6 +32,27 @@ fn count_nitrokey_block_devices() -> usize {
}
#[test_device]
+fn list_no_devices() {
+ let devices = nitrokey::list_devices();
+ assert_ok!(Vec::<DeviceInfo>::new(), devices);
+}
+
+#[test_device]
+fn list_devices(_device: DeviceWrapper) {
+ let devices = unwrap_ok!(nitrokey::list_devices());
+ for device in devices {
+ assert!(!device.path.is_empty());
+ if Some(nitrokey::Model::Storage) == device.model {
+ assert_eq!(None, device.serial_number);
+ } else {
+ assert!(device.serial_number.is_some());
+ let serial_number = device.serial_number.unwrap();
+ assert!(serial_number != SerialNumber::empty());
+ }
+ }
+}
+
+#[test_device]
fn connect_no_device() {
let mut manager = unwrap_ok!(nitrokey::take());
@@ -78,17 +99,65 @@ fn assert_empty_serial_number() {
}
#[test_device]
+fn connect_path_no_device() {
+ let mut manager = unwrap_ok!(nitrokey::take());
+
+ assert_cmu_err!(CommunicationError::NotConnected, manager.connect_path(""));
+ assert_cmu_err!(
+ CommunicationError::NotConnected,
+ manager.connect_path("foobar")
+ );
+ // TODO: add realistic path
+}
+
+#[test_device]
+fn connect_path(device: DeviceWrapper) {
+ let manager = device.into_manager();
+
+ assert_cmu_err!(CommunicationError::NotConnected, manager.connect_path(""));
+ assert_cmu_err!(
+ CommunicationError::NotConnected,
+ manager.connect_path("foobar")
+ );
+ // TODO: add realistic path
+
+ let devices = unwrap_ok!(nitrokey::list_devices());
+ assert!(!devices.is_empty());
+ for device in devices {
+ let connected_device = unwrap_ok!(manager.connect_path(device.path));
+ assert_eq!(device.model, Some(connected_device.get_model()));
+ if nitrokey::Model::Storage == device.model.unwrap() {
+ assert_eq!(None, device.serial_number);
+ } else {
+ assert!(device.serial_number.is_some());
+ assert_ok!(
+ device.serial_number.unwrap(),
+ connected_device.get_serial_number()
+ );
+ }
+ }
+}
+
+#[test_device]
fn disconnect(device: DeviceWrapper) {
drop(device);
assert_empty_serial_number();
}
#[test_device]
+fn get_status(device: DeviceWrapper) {
+ let status = unwrap_ok!(device.get_status());
+ assert_ok!(status.firmware_version, device.get_firmware_version());
+ assert_ok!(status.serial_number, device.get_serial_number());
+ assert_ok!(status.config, device.get_config());
+}
+
+#[test_device]
fn get_serial_number(device: DeviceWrapper) {
let serial_number = unwrap_ok!(device.get_serial_number());
- assert!(serial_number.is_ascii());
- assert!(serial_number.chars().all(|c| c.is_ascii_hexdigit()));
+ assert!(serial_number != SerialNumber::empty());
}
+
#[test_device]
fn get_firmware_version(device: Pro) {
let version = unwrap_ok!(device.get_firmware_version());
@@ -480,7 +549,7 @@ fn set_encrypted_volume_mode(device: Storage) {
#[test_device]
fn set_unencrypted_volume_mode(device: Storage) {
fn assert_mode(device: &Storage, mode: VolumeMode) {
- let status = unwrap_ok!(device.get_status());
+ let status = unwrap_ok!(device.get_storage_status());
assert_eq!(
status.unencrypted_volume.read_only,
mode == VolumeMode::ReadOnly
@@ -511,7 +580,7 @@ fn set_unencrypted_volume_mode(device: Storage) {
#[test_device]
fn get_storage_status(device: Storage) {
- let status = unwrap_ok!(device.get_status());
+ let status = unwrap_ok!(device.get_storage_status());
assert!(status.serial_number_sd_card > 0);
assert!(status.serial_number_smart_card > 0);
}
@@ -531,7 +600,7 @@ fn get_production_info(device: Storage) {
assert!(info.sd_card.oem != 0);
assert!(info.sd_card.manufacturer != 0);
- let status = unwrap_ok!(device.get_status());
+ let status = unwrap_ok!(device.get_storage_status());
assert_eq!(status.firmware_version, info.firmware_version);
assert_eq!(status.serial_number_sd_card, info.sd_card.serial_number);
}
@@ -546,16 +615,68 @@ fn clear_new_sd_card_warning(device: Storage) {
// We have to perform an SD card operation to reset the new_sd_card_found field
assert_ok!((), device.lock());
- let status = unwrap_ok!(device.get_status());
+ let status = unwrap_ok!(device.get_storage_status());
assert!(status.new_sd_card_found);
assert_ok!((), device.clear_new_sd_card_warning(DEFAULT_ADMIN_PIN));
- let status = unwrap_ok!(device.get_status());
+ let status = unwrap_ok!(device.get_storage_status());
assert!(!status.new_sd_card_found);
}
#[test_device]
+fn get_sd_card_usage(device: Storage) {
+ let range = unwrap_ok!(device.get_sd_card_usage());
+
+ assert!(range.end >= range.start);
+ assert!(range.end <= 100);
+}
+
+#[test_device]
+fn get_operation_status(device: Storage) {
+ assert_ok!(OperationStatus::Idle, device.get_operation_status());
+}
+
+#[test_device]
+#[ignore]
+fn fill_sd_card(device: Storage) {
+ // This test takes up to 60 min to execute and is therefore ignored by default. Use `cargo
+ // test -- --ignored fill_sd_card` to run the test.
+
+ let mut device = device;
+ assert_ok!((), device.factory_reset(DEFAULT_ADMIN_PIN));
+ thread::sleep(time::Duration::from_secs(3));
+ assert_ok!((), device.build_aes_key(DEFAULT_ADMIN_PIN));
+
+ let status = unwrap_ok!(device.get_storage_status());
+ assert!(!status.filled_with_random);
+
+ assert_ok!((), device.fill_sd_card(DEFAULT_ADMIN_PIN));
+ assert_cmd_err!(CommandError::WrongCrc, device.get_status());
+
+ let mut status = OperationStatus::Ongoing(0);
+ let mut last_progress = 0u8;
+ while status != OperationStatus::Idle {
+ status = unwrap_ok!(device.get_operation_status());
+ if let OperationStatus::Ongoing(progress) = status {
+ assert!(progress <= 100, "progress = {}", progress);
+ assert!(
+ progress >= last_progress,
+ "progress = {}, last_progress = {}",
+ progress,
+ last_progress
+ );
+ last_progress = progress;
+
+ thread::sleep(time::Duration::from_secs(10));
+ }
+ }
+
+ let status = unwrap_ok!(device.get_storage_status());
+ assert!(status.filled_with_random);
+}
+
+#[test_device]
fn export_firmware(device: Storage) {
let mut device = device;
assert_cmd_err!(
diff --git a/tests/otp.rs b/tests/otp.rs
index aafda59..38cd8a9 100644
--- a/tests/otp.rs
+++ b/tests/otp.rs
@@ -44,12 +44,12 @@ where
unwrap_ok!(device.authenticate_admin(DEFAULT_ADMIN_PIN))
}
-fn configure_hotp(admin: &mut ConfigureOtp, counter: u8) {
+fn configure_hotp(admin: &mut dyn ConfigureOtp, counter: u8) {
let slot_data = OtpSlotData::new(1, "test-hotp", HOTP_SECRET, OtpMode::SixDigits);
assert_ok!((), admin.write_hotp_slot(slot_data, counter.into()));
}
-fn check_hotp_codes(device: &mut GenerateOtp, offset: u8) {
+fn check_hotp_codes(device: &mut dyn GenerateOtp, offset: u8) {
HOTP_CODES.iter().enumerate().for_each(|(i, code)| {
if i >= offset as usize {
assert_ok!(code.to_string(), device.get_hotp_code(1));
@@ -146,13 +146,13 @@ fn hotp_erase(device: DeviceWrapper) {
assert_ok!("test2".to_string(), device.get_hotp_slot_name(2));
}
-fn configure_totp(admin: &mut ConfigureOtp, factor: u64) {
+fn configure_totp(admin: &mut dyn ConfigureOtp, factor: u64) {
let slot_data = OtpSlotData::new(1, "test-totp", TOTP_SECRET, OtpMode::EightDigits);
let time_window = 30u64.checked_mul(factor).unwrap();
assert_ok!((), admin.write_totp_slot(slot_data, time_window as u16));
}
-fn check_totp_codes(device: &mut GenerateOtp, factor: u64, timestamp_size: TotpTimestampSize) {
+fn check_totp_codes(device: &mut dyn GenerateOtp, factor: u64, timestamp_size: TotpTimestampSize) {
for (base_time, codes) in TOTP_CODES {
let time = base_time.checked_mul(factor).unwrap();
let is_u64 = time > u32::max_value() as u64;
diff --git a/tests/pws.rs b/tests/pws.rs
index 7169695..47e9703 100644
--- a/tests/pws.rs
+++ b/tests/pws.rs
@@ -16,7 +16,7 @@ use nitrokey_test::test as test_device;
fn get_slot_name_direct(slot: u8) -> Result<String, Error> {
let ptr = unsafe { nitrokey_sys::NK_get_password_safe_slot_name(slot) };
if ptr.is_null() {
- return Err(Error::UnexpectedError);
+ return Err(Error::UnexpectedError("null pointer".to_owned()));
}
let s = unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() };
unsafe { free(ptr as *mut c_void) };