From 681cc8882f7995407c33eb48730daaa901074460 Mon Sep 17 00:00:00 2001 From: Daniel Mueller Date: Sat, 4 Apr 2020 15:32:14 -0700 Subject: Move nitrocli source code into repository root Now that all vendored dependencies have been removed, this change moves the program's source code from the nitrocli/ directory into the root of the repository. --- .gitignore | 2 + CHANGELOG.md | 148 ++++++ Cargo.lock | 193 ++++++++ Cargo.toml | 64 +++ LICENSE | 674 ++++++++++++++++++++++++++ Makefile | 70 +++ README.md | 166 +++++++ ci/gitlab-ci.yml | 26 + doc/CONTRIBUTING.md | 23 + doc/nitrocli.1 | 355 ++++++++++++++ doc/nitrocli.1.pdf | Bin 0 -> 18442 bytes doc/packaging.md | 64 +++ nitrocli/.gitignore | 2 - nitrocli/CHANGELOG.md | 147 ------ nitrocli/Cargo.lock | 193 -------- nitrocli/Cargo.toml | 64 --- nitrocli/LICENSE | 674 -------------------------- nitrocli/Makefile | 70 --- nitrocli/README.md | 167 ------- nitrocli/ci/gitlab-ci.yml | 36 -- nitrocli/doc/CONTRIBUTING.md | 23 - nitrocli/doc/nitrocli.1 | 355 -------------- nitrocli/doc/nitrocli.1.pdf | Bin 18442 -> 0 bytes nitrocli/doc/packaging.md | 64 --- nitrocli/rustfmt.toml | 1 - nitrocli/src/arg_util.rs | 158 ------ nitrocli/src/args.rs | 984 -------------------------------------- nitrocli/src/commands.rs | 984 -------------------------------------- nitrocli/src/error.rs | 104 ---- nitrocli/src/main.rs | 167 ------- nitrocli/src/pinentry.rs | 404 ---------------- nitrocli/src/redefine.rs | 38 -- nitrocli/src/tests/config.rs | 66 --- nitrocli/src/tests/encrypted.rs | 95 ---- nitrocli/src/tests/hidden.rs | 49 -- nitrocli/src/tests/lock.rs | 44 -- nitrocli/src/tests/mod.rs | 180 ------- nitrocli/src/tests/otp.rs | 130 ----- nitrocli/src/tests/pin.rs | 84 ---- nitrocli/src/tests/pws.rs | 123 ----- nitrocli/src/tests/reset.rs | 60 --- nitrocli/src/tests/run.rs | 103 ---- nitrocli/src/tests/status.rs | 81 ---- nitrocli/src/tests/unencrypted.rs | 46 -- nitrocli/var/binary-size.py | 134 ------ rustfmt.toml | 1 + src/arg_util.rs | 158 ++++++ src/args.rs | 984 ++++++++++++++++++++++++++++++++++++++ src/commands.rs | 984 ++++++++++++++++++++++++++++++++++++++ src/error.rs | 104 ++++ src/main.rs | 167 +++++++ src/pinentry.rs | 404 ++++++++++++++++ src/redefine.rs | 38 ++ src/tests/config.rs | 66 +++ src/tests/encrypted.rs | 95 ++++ src/tests/hidden.rs | 49 ++ src/tests/lock.rs | 44 ++ src/tests/mod.rs | 180 +++++++ src/tests/otp.rs | 130 +++++ src/tests/pin.rs | 84 ++++ src/tests/pws.rs | 123 +++++ src/tests/reset.rs | 60 +++ src/tests/run.rs | 103 ++++ src/tests/status.rs | 81 ++++ src/tests/unencrypted.rs | 46 ++ var/binary-size.py | 134 ++++++ 66 files changed, 5820 insertions(+), 5830 deletions(-) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 ci/gitlab-ci.yml create mode 100644 doc/CONTRIBUTING.md create mode 100644 doc/nitrocli.1 create mode 100644 doc/nitrocli.1.pdf create mode 100644 doc/packaging.md delete mode 100644 nitrocli/.gitignore delete mode 100644 nitrocli/CHANGELOG.md delete mode 100644 nitrocli/Cargo.lock delete mode 100644 nitrocli/Cargo.toml delete mode 100644 nitrocli/LICENSE delete mode 100644 nitrocli/Makefile delete mode 100644 nitrocli/README.md delete mode 100644 nitrocli/ci/gitlab-ci.yml delete mode 100644 nitrocli/doc/CONTRIBUTING.md delete mode 100644 nitrocli/doc/nitrocli.1 delete mode 100644 nitrocli/doc/nitrocli.1.pdf delete mode 100644 nitrocli/doc/packaging.md delete mode 100644 nitrocli/rustfmt.toml delete mode 100644 nitrocli/src/arg_util.rs delete mode 100644 nitrocli/src/args.rs delete mode 100644 nitrocli/src/commands.rs delete mode 100644 nitrocli/src/error.rs delete mode 100644 nitrocli/src/main.rs delete mode 100644 nitrocli/src/pinentry.rs delete mode 100644 nitrocli/src/redefine.rs delete mode 100644 nitrocli/src/tests/config.rs delete mode 100644 nitrocli/src/tests/encrypted.rs delete mode 100644 nitrocli/src/tests/hidden.rs delete mode 100644 nitrocli/src/tests/lock.rs delete mode 100644 nitrocli/src/tests/mod.rs delete mode 100644 nitrocli/src/tests/otp.rs delete mode 100644 nitrocli/src/tests/pin.rs delete mode 100644 nitrocli/src/tests/pws.rs delete mode 100644 nitrocli/src/tests/reset.rs delete mode 100644 nitrocli/src/tests/run.rs delete mode 100644 nitrocli/src/tests/status.rs delete mode 100644 nitrocli/src/tests/unencrypted.rs delete mode 100755 nitrocli/var/binary-size.py create mode 100644 rustfmt.toml create mode 100644 src/arg_util.rs create mode 100644 src/args.rs create mode 100644 src/commands.rs create mode 100644 src/error.rs create mode 100644 src/main.rs create mode 100644 src/pinentry.rs create mode 100644 src/redefine.rs create mode 100644 src/tests/config.rs create mode 100644 src/tests/encrypted.rs create mode 100644 src/tests/hidden.rs create mode 100644 src/tests/lock.rs create mode 100644 src/tests/mod.rs create mode 100644 src/tests/otp.rs create mode 100644 src/tests/pin.rs create mode 100644 src/tests/pws.rs create mode 100644 src/tests/reset.rs create mode 100644 src/tests/run.rs create mode 100644 src/tests/status.rs create mode 100644 src/tests/unencrypted.rs create mode 100755 var/binary-size.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6262ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +*.swp diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..531a38e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,148 @@ +Unreleased +---------- +- Removed vendored dependencies and moved source code into repository + root + + +0.3.1 +----- +- Added note about interaction with GnuPG to `README` file +- Bumped `nitrokey` dependency to `0.4.0` + - Bumped `nitrokey-sys` dependency to `3.5.0` + - Added `lazy_static` dependency in version `1.4.0` + - Added `cfg-if` dependency in version `0.1.10` + - Added `getrandom` dependency in version `0.1.13` + + +0.3.0 +----- +- Added `unencrypted` command with `set` subcommand for changing the + unencrypted volume's read-write mode +- Changed `storage hidden` subcommand to `hidden` top-level command +- Renamed `storage` command to `encrypted` +- Removed `storage status` subcommand + - Moved its output into `status` command +- Removed previously deprecated `--ascii` option from `otp set` command +- Fixed wrong hexadecimal conversion used in `otp set` command +- Bumped `nitrokey` dependency to `0.3.5` +- Bumped `libc` dependency to `0.2.66` +- Bumped `cc` dependency to `1.0.48` + + +0.2.4 +----- +- Added the `reset` command to perform a factory reset +- Added the `-V`/`--version` option to print the program's version +- Check the status of a PWS slot before accessing it in `pws get` +- Added `NITROCLI_NO_CACHE` environment variable to bypass caching of + secrets +- Clear cached PIN entry as part of `pin set` command to prevent + spurious authentication failures +- Bumped `libc` dependency to `0.2.57` +- Bumped `cc` dependency to `1.0.37` + + +0.2.3 +----- +- Added the `storage hidden` subcommand for working with hidden volumes +- Store cached PINs on a per-device basis to better support multi-device + scenarios +- Further decreased binary size by using system allocator +- Bumped `nitrokey` dependency to `0.3.4` + - Bumped `rand` dependency to `0.6.4` + - Removed `rustc_version`, `semver`, and `semver-parser` dependencies +- Bumped `nitrokey-sys` dependency to `3.4.3` +- Bumped `libc` dependency to `0.2.47` + + +0.2.2 +----- +- Added the `-v`/`--verbose` option to control libnitrokey log level +- Added the `-m`/`--model` option to restrict connections to a device + model +- Added the `-f`/`--format` option for the `otp set` subcommand to + choose the secret format + - Deprecated the `--ascii` option +- Honor `NITROCLI_ADMIN_PIN` and `NITROCLI_USER_PIN` as well as + `NITROCLI_NEW_ADMIN_PIN` and `NITROCLI_NEW_USER_PIN` environment + variables for non-interactive PIN supply +- Format `nitrokey` reported errors in more user-friendly format +- Bumped `nitrokey` dependency to `0.3.1` + + +0.2.1 +----- +- Added the `pws` command for accessing the password safe +- Added the `lock` command for locking the Nitrokey device +- Adjusted release build compile options to optimize binary for size +- Bumped `nitrokey` dependency to `0.2.3` + - Bumped `rand` dependency to `0.6.1` + - Added `rustc_version` version `0.2.3`, `semver` version `0.9.0`, and + `semver-parser` version `0.7.0` as indirect dependencies +- Bumped `cc` dependency to `1.0.28` + + +0.2.0 +----- +- Use the `nitrokey` crate for the `open`, `close`, and `status` + commands instead of directly communicating with the Nitrokey device + - Added `nitrokey` version `0.2.1` as a direct dependency and + `nitrokey-sys` version `3.4.1` as well as `rand` version `0.4.3` as + indirect dependencies + - Removed the `hid`, `hidapi-sys` and `pkg-config` dependencies +- Added the `otp` command for working with one-time passwords +- Added the `config` command for reading and writing the device configuration +- Added the `pin` command for managing PINs + - Renamed the `clear` command to `pin clear` +- Moved `open` and `close` commands as subcommands into newly introduced + `storage` command + - Moved printing of storage related information from `status` command + into new `storage status` subcommand +- Made `status` command work with Nitrokey Pro devices +- Enabled CI pipeline comprising code style conformance checks, linting, + and building of the project +- Added badges indicating pipeline status, current `crates.io` published + version of the crate, and minimum version of `rustc` required +- Fixed wrong messages in the pinentry dialog that were caused by unescaped + spaces in a string +- Use the `argparse` crate to parse the command-line arguments + - Added `argparse` dependency in version `0.2.2` + + +0.1.3 +----- +- Show PIN related errors through `pinentry` native reporting mechanism + instead of emitting them to `stdout` +- Added a `man` page (`nitrocli(1)`) for the program to the repository +- Adjusted program to use Rust Edition 2018 +- Enabled more lints +- Applied a couple of `clippy` reported suggestions +- Added categories to `Cargo.toml` +- Changed dependency version requirements to be less strict (only up to + the minor version and not the patch level) +- Bumped `pkg-config` dependency to `0.3.14` +- Bumped `libc` dependency to `0.2.45` +- Bumped `cc` dependency to `1.0.25` + + +0.1.2 +----- +- Replaced deprecated `gcc` dependency with `cc` and bumped to `1.0.4` +- Bumped `hid` dependency to `0.4.1` +- Bumped `hidapi-sys` dependency to `0.1.4` +- Bumped `libc` dependency to `0.2.36` + + +0.1.1 +----- +- Fixed display of firmware version for `status` command +- Removed workaround for incorrect CRC checksum produced by the Nitrokey + Storage device + - The problem has been fixed upstream (`nitrokey-storage-firmware` + [issue #32](https://github.com/Nitrokey/nitrokey-storage-firmware/issues/32)) + - In order to be usable, a minimum firmware version of 0.47 is required + + +0.1.0 +----- +- Initial release diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3e914a4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,193 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "argparse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cc" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "getrandom" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "nitrocli" +version = "0.3.1" +dependencies = [ + "argparse 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "base32 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "nitrokey 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "nitrokey-test 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "nitrokey-test-state 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nitrokey" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "nitrokey-sys 3.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nitrokey-sys" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nitrokey-test" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nitrokey-test-state" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro2" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "wasi" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" +"checksum argparse 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47" +"checksum base32 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" +"checksum cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "f52a465a666ca3d838ebbf08b241383421412fe7ebb463527bba275526d89f76" +"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +"checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407" +"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" +"checksum nitrokey 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a5f19c16e1530850b171e98ad1c95fe0af897916eb308e19fbfd3ee2dcda0abe" +"checksum nitrokey-sys 3.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "71c6052aeb37309317d25c8fec2801f271b96c5a15656f2573d8e78ba4124c49" +"checksum nitrokey-test 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f3da0c2cedaa512f79fbc3ed45143a52c76c5edcca88d0823b967ff11d05fe37" +"checksum nitrokey-test-state 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a59b732ed6d5212424ed31ec9649f05652bcbc38f45f2292b27a6044e7098803" +"checksum proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0319972dcae462681daf4da1adeeaa066e3ebd29c69be96c6abb1259d2ee2bcc" +"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +"checksum regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd" +"checksum regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716" +"checksum syn 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ddc157159e2a7df58cd67b1cace10b8ed256a404fb0070593f137d8ba6bef4de" +"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..63e6409 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,64 @@ +# Cargo.toml + +#/*************************************************************************** +# * Copyright (C) 2017-2020 Daniel Mueller (deso@posteo.net) * +# * * +# * This program is free software: you can redistribute it and/or modify * +# * it under the terms of the GNU General Public License as published by * +# * the Free Software Foundation, either version 3 of the License, or * +# * (at your option) any later version. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU General Public License for more details. * +# * * +# * You should have received a copy of the GNU General Public License * +# * along with this program. If not, see . * +# ***************************************************************************/ + +[package] +name = "nitrocli" +version = "0.3.1" +edition = "2018" +authors = ["Daniel Mueller "] +license = "GPL-3.0-or-later" +homepage = "https://github.com/d-e-s-o/nitrocli" +repository = "https://github.com/d-e-s-o/nitrocli.git" +readme = "README.md" +categories = ["command-line-utilities", "authentication", "cryptography", "hardware-support"] +keywords = ["nitrokey", "nitrokey-storage", "nitrokey-pro", "cli", "usb"] +description = """ +A command line tool for interacting with Nitrokey devices. +""" +exclude = ["ci/*", "rustfmt.toml"] + +[badges] +gitlab = { repository = "d-e-s-o/nitrocli", branch = "master" } + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +incremental = false + +[dependencies.argparse] +version = "0.2.2" + +[dependencies.base32] +version = "0.4.0" + +[dependencies.libc] +version = "0.2" + +[dependencies.nitrokey] +version = "0.4.0" + +[dev-dependencies.nitrokey-test] +version = "0.3.1" + +[dev-dependencies.nitrokey-test-state] +version = "0.1" + +[dev-dependencies.regex] +version = "1" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ef7e7ef --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e7f7da5 --- /dev/null +++ b/Makefile @@ -0,0 +1,70 @@ +# Makefile + +#/*************************************************************************** +# * Copyright (C) 2017-2019 Daniel Mueller (deso@posteo.net) * +# * * +# * This program is free software: you can redistribute it and/or modify * +# * it under the terms of the GNU General Public License as published by * +# * the Free Software Foundation, either version 3 of the License, or * +# * (at your option) any later version. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU General Public License for more details. * +# * * +# * You should have received a copy of the GNU General Public License * +# * along with this program. If not, see . * +# ***************************************************************************/ + +SHELL := bash + +PS2PDF ?= ps2pdf + +NITROCLI_MAN := doc/nitrocli.1 +NITROCLI_PDF := $(addsuffix .pdf,$(NITROCLI_MAN)) + +.PHONY: doc +doc: $(NITROCLI_PDF) $(NITROCLI_HTML) + +# We assume and do not check existence of man, which, false, and echo +# commands. +$(NITROCLI_PDF): $(NITROCLI_MAN) + @which $(PS2PDF) &> /dev/null || \ + (echo "$(PS2PDF) command not found, unable to generate documentation"; false) + @man --local-file --troff $(<) | $(PS2PDF) - $(@) + +KEY ?= 0x952DD6F8F34D8B8E + +.PHONY: sign +sign: + @test -n "$(REL)" || \ + (echo "Please set REL environment variable to the release to verify (e.g., '0.2.1')."; false) + @mkdir -p pkg/ + wget --quiet "https://github.com/d-e-s-o/nitrocli/archive/v$(REL).zip" \ + -O "pkg/nitrocli-$(REL).zip" + @set -euo pipefail && DIR1=$$(mktemp -d) && DIR2=$$(mktemp -d) && \ + unzip -q pkg/nitrocli-$(REL).zip -d $${DIR1} && \ + git -C $$(git rev-parse --show-toplevel) archive --prefix=nitrocli-$(REL)/ v$(REL) | \ + tar -x -C $${DIR2} && \ + diff -u -r $${DIR1} $${DIR2} && \ + echo "Github zip archive verified successfully" && \ + (rm -r $${DIR1} && rm -r $${DIR2}) + wget --quiet "https://github.com/d-e-s-o/nitrocli/archive/v$(REL).tar.gz" \ + -O "pkg/nitrocli-$(REL).tar.gz" + @set -euo pipefail && DIR1=$$(mktemp -d) && DIR2=$$(mktemp -d) && \ + tar -xz -C $${DIR1} -f pkg/nitrocli-$(REL).tar.gz && \ + git -C $$(git rev-parse --show-toplevel) archive --prefix=nitrocli-$(REL)/ v$(REL) | \ + tar -x -C $${DIR2} && \ + diff -u -r $${DIR1} $${DIR2} && \ + echo "Github tarball verified successfully" && \ + (rm -r $${DIR1} && rm -r $${DIR2}) + @cd pkg && sha256sum nitrocli-$(REL).tar.gz nitrocli-$(REL).zip > nitrocli-$(REL).sha256.DIGEST + @gpg --sign --armor --detach-sign --default-key=$(KEY) --yes \ + --output pkg/nitrocli-$(REL).sha256.DIGEST.sig pkg/nitrocli-$(REL).sha256.DIGEST + @gpg --verify pkg/nitrocli-$(REL).sha256.DIGEST.sig + @cd pkg && sha256sum --check < nitrocli-$(REL).sha256.DIGEST + @echo "All checks successful. Please attach" + @echo " pkg/nitrocli-$(REL).sha256.DIGEST" + @echo " pkg/nitrocli-$(REL).sha256.DIGEST.sig" + @echo "to https://github.com/d-e-s-o/nitrocli/releases/tag/v$(REL)" diff --git a/README.md b/README.md new file mode 100644 index 0000000..f76216f --- /dev/null +++ b/README.md @@ -0,0 +1,166 @@ +[![pipeline](https://gitlab.com/d-e-s-o/nitrocli/badges/master/pipeline.svg)](https://gitlab.com/d-e-s-o/nitrocli/commits/master) +[![crates.io](https://img.shields.io/crates/v/nitrocli.svg)](https://crates.io/crates/nitrocli) +[![rustc](https://img.shields.io/badge/rustc-1.35+-blue.svg)](https://blog.rust-lang.org/2019/05/23/Rust-1.35.0.html) + +nitrocli +======== + +- [Changelog](CHANGELOG.md) + +**nitrocli** is a program that provides a command line interface for +interaction with [Nitrokey Pro][nitrokey-pro] and [Nitrokey +Storage][nitrokey-storage] devices. + + +The following commands are currently supported: +- status: Report status information about the Nitrokey. +- lock: Lock the Nitrokey. +- config: Access the Nitrokey's configuration + - get: Read the current configuration. + - set: Change the configuration. +- encrypted: Work with the Nitrokey Storage's encrypted volume. + - open: Open the encrypted volume. The user PIN needs to be entered. + - close: Close the encrypted volume. +- hidden: Work with the Nitrokey Storage's hidden volume. + - create: Create a hidden volume. + - open: Open a hidden volume with a password. + - close: Close a hidden volume. +- otp: Access one-time passwords (OTP). + - get: Generate a one-time password. + - set: Set an OTP slot. + - status: List all OTP slots. + - clear: Delete an OTP slot. +- pin: Manage the Nitrokey's PINs. + - clear: Remove the user and admin PIN from gpg-agent's cache. + - set: Change the admin or the user PIN. + - unblock: Unblock and reset the user PIN. +- pws: Access the password safe (PWS). + - get: Query the data on a PWS slot. + - set: Set the data on a PWS slot. + - status: List all PWS slots. + - clear: Delete a PWS slot. +- unencrypted: Work with the Nitrokey Storage's unencrypted volume. + - set: Change the read-write mode of the unencrypted volume. + + +Usage +----- + +Usage is as simple as providing the name of the respective command as a +parameter (note that some commands are organized through subcommands, +which are required as well), e.g.: +```bash +# Open the nitrokey's encrypted volume. +$ nitrocli storage open + +$ nitrocli status +Status: + model: Storage + serial number: 0x00053141 + firmware version: 0.47 + user retry count: 3 + admin retry count: 3 + Storage: + SD card ID: 0x05dcad1d + firmware: unlocked + storage keys: created + volumes: + unencrypted: active + encrypted: active + hidden: inactive + +# Close it again. +$ nitrocli storage close +``` + +More examples, a more detailed explanation of the purpose, the potential +subcommands, as well as the parameters of each command are provided in +the [`man` page](doc/nitrocli.1.pdf). + + +Installation +------------ + +In addition to Rust itself and Cargo, its package management tool, the +following dependencies are required: +- **hidapi**: In order to provide USB access this library is used. +- **GnuPG**: The `gpg-connect-agent` program allows the user to enter + PINs. + +#### Via Packages +Packages are available for: +- Arch Linux: [`nitrocli`][nitrocli-arch] in the Arch User Repository +- Debian: [`nitrocli`][nitrocli-debian] (since Debian Buster) +- Gentoo Linux: [`app-crypt/nitrocli`][nitrocli-gentoo] ebuild +- Ubuntu: [`nitrocli`][nitrocli-ubuntu] (since Ubuntu 19.04) + +#### From Crates.io +**nitrocli** is [published][nitrocli-cratesio] on crates.io and can +directly be installed from there: +```bash +$ cargo install nitrocli --root=$PWD/nitrocli +``` + +#### From Source +After cloning the repository the build is as simple as running: +```bash +$ cargo build --release +``` + +It is recommended that the resulting executable be installed in a +directory accessible via the `PATH` environment variable. + + +Known Problems +-------------- + +- Due to a problem with the default `hidapi` version on macOS, users are + advised to build and install [`libnitrokey`][] from source and then + set the `USE_SYSTEM_LIBNITROKEY` environment variable when building + `nitrocli` using one of the methods described above. +- `nitrocli` cannot connect to a Nitrokey device that is currently being + accessed by `nitrokey-app` ([upstream issue][libnitrokey#32]). To + prevent this problem, quit `nitrokey-app` before using `nitrocli`. +- Applications using the Nitrokey device (such as `nitrocli` or + `nitrokey-app`) cannot easily share access with an instance of GnuPG + running shortly afterwards ([upstream issue][libnitrokey#137]). + + +Contributing +------------ + +Contributions are generally welcome. Please follow the guidelines +outlined in [CONTRIBUTING.md](doc/CONTRIBUTING.md). + + +Acknowledgments +--------------- + +Robin Krahl ([@robinkrahl](https://github.com/robinkrahl)) has been +a crucial help for the development of **nitrocli**. + +The [Nitrokey UG][nitrokey-ug] has generously provided the necessary +hardware for developing and testing the program. + + +License +------- +**nitrocli** is made available under the terms of the +[GPLv3][gplv3-tldr]. + +See the [LICENSE](LICENSE) file that accompanies this distribution for +the full text of the license. + + +[`libnitrokey`]: https://github.com/nitrokey/libnitrokey +[nitrokey-ug]: https://www.nitrokey.com +[nitrokey-pro]: https://shop.nitrokey.com/shop/product/nitrokey-pro-2-3 +[nitrokey-storage]: https://shop.nitrokey.com/shop/product/nitrokey-storage-2-16gb-23 +[nitrocli-arch]: https://aur.archlinux.org/packages/nitrocli +[nitrocli-cratesio]: https://crates.io/crates/nitrocli +[nitrocli-debian]: https://packages.debian.org/stable/nitrocli +[nitrocli-gentoo]: https://packages.gentoo.org/packages/app-crypt/nitrocli +[nitrocli-ubuntu]: https://packages.ubuntu.com/search?keywords=nitrocli +[gplv3-tldr]: https://tldrlegal.com/license/gnu-general-public-license-v3-(gpl-3) +[libnitrokey#32]: https://github.com/Nitrokey/libnitrokey/issues/32 +[libnitrokey#137]: https://github.com/Nitrokey/libnitrokey/issues/137 diff --git a/ci/gitlab-ci.yml b/ci/gitlab-ci.yml new file mode 100644 index 0000000..729944d --- /dev/null +++ b/ci/gitlab-ci.yml @@ -0,0 +1,26 @@ +# The documentation for the contents of this file can be found at: +# https://docs.gitlab.com/ce/ci/yaml/README.html + +# Official language image. Look for the different tagged releases at: +# https://hub.docker.com/r/library/rust/tags/ +# The recipe for this docker image can be found at: +# https://github.com/rust-lang/docker-rust/blob/29bf41a2cc4fb8d3f588cf51eb6a8ba883808c4b/1.35.0/stretch/Dockerfile +image: "rust:1.35.0" + +build-test:cargo: + script: + - apt-get update + - apt-get install --assume-yes libudev-dev libhidapi-dev + - rustc --version && cargo --version + - cargo build --all --verbose + - cargo test --all --verbose + +lint:clippy: + script: + - rustup component add clippy + - cargo clippy --all-targets --all-features -- -D warnings + +format:rustfmt: + script: + - rustup component add rustfmt + - cargo fmt --all -- --check diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md new file mode 100644 index 0000000..3ebdfce --- /dev/null +++ b/doc/CONTRIBUTING.md @@ -0,0 +1,23 @@ +The following rules generally apply for pull requests and code changes: + +**Submit Pull Requests to the `devel` branch** + +The `devel` branch is where experimental features reside. After some +soak time they may be ported over to `master` and a release will be cut +that includes them. + +**Keep documentation up-to-date** + +Please make an effort to keep the documentation up-to-date to the extent +possible and necessary for the change at hand. That includes adjusting +the [README](../README.md) and [`man` page](nitrocli.1) as well as +regenerating the PDF rendered version of the latter by running `make +doc`. + +**Blend with existing patterns and style** + +To keep the code as consistent as possible, please try not to diverge +from the existing style used in a file. Specifically for Rust source +code, use [`rustfmt`](https://github.com/rust-lang/rustfmt) and +[`clippy`](https://github.com/rust-lang/rust-clippy) to achieve a +minimum level of consistency and prevent known bugs, respectively. diff --git a/doc/nitrocli.1 b/doc/nitrocli.1 new file mode 100644 index 0000000..75f5960 --- /dev/null +++ b/doc/nitrocli.1 @@ -0,0 +1,355 @@ +.TH NITROCLI 1 2019-06-08 +.SH NAME +nitrocli \- access Nitrokey devices +.SH SYNOPSIS +.B nitrocli +[\fB\-m\fR|\fB\-\-model pro\fR|\fBstorage\fR] \fR[\fB\-v\fR|\fB\-\-verbose\fR] +[\fB\-V\fR|\fB\-\-version\fR] +\fIcommand\fR +[\fIarguments\fR] +.SH DESCRIPTION +\fBnitrocli\fR provides access to Nitrokey devices. +It supports the Nitrokey Pro and the Nitrokey Storage. +It can be used to access the encrypted volume, the one-time password generator, +and the password safe. +.SH OPTIONS +.TP +\fB\-m\fR, \fB\-\-model pro\fR|\fBstorage\fR +Restrict connections to the given device model. +If this option is not set, nitrocli will connect to any connected Nitrokey Pro +or Nitrokey Storage device. +.TP +\fB\-v\fR, \fB\-\-verbose\fR +Enable additional logging and control its verbosity. Logging enabled through +this option will appear on the standard error stream. This option can be +supplied multiple times. A single occurrence will show additional warnings. +Commands sent to the device will be shown when supplied three times and full +device communication is available with four occurrences. Supplying this option +five times enables the highest verbosity. +.TP +\fB\-V\fR, \fB\-\-version\fR +Print the nitrocli version and exit. +.SH COMMANDS +.SS General +.TP +.B nitrocli status +Print the status of the connected Nitrokey device, including the stick serial +number, the firmware version, and the PIN retry count. If the device is a +Nitrokey Storage, also print storage related information including the SD card +serial number, the encryption status, and the status of the volumes. +.TP +.B nitrocli lock +Lock the Nitrokey. +This command locks the password safe (see the Password safe section). On the +Nitrokey Storage, it will also close any active encrypted or hidden volumes (see +the Storage section). +.TP +.B nitrocli reset +Perform a factory reset on the Nitrokey. +This command performs a factory reset on the OpenPGP smart card, clears the +flash storage and builds a new AES key. +The user PIN is reset to 123456, the admin PIN to 12345678. + +This command requires the admin PIN. +To avoid accidental calls of this command, the user has to enter the PIN even +if it has been cached. + +.SS Storage +The Nitrokey Storage comes with a storage area. This area is comprised of an +\fIunencrypted\fR region and an \fIencrypted\fR one of fixed sizes, each made +available to the user in the form of block devices. The encrypted region can +optionally further be overlayed with up to four \fIhidden\fR volumes. Because of +this overlay (which is required to achieve plausible deniability of the +existence of hidden volumes), the burden of ensuring that data on the encrypted +volume does not overlap with data on one of the hidden volumes is on the user. +.TP +\fBnitrocli unencrypted set \fImode\fR +Change the read-write mode of the volume. +\fImode\fR is the type of the mode to change to: \fBread-write\fR to make the +volume readable and writable or \fBread-only\fR to make it only readable. +This command requires the admin PIN. + +Note that this command requires firmware version 0.51 or higher. Earlier +versions are not supported. +.TP +\fBnitrocli encrypted open +Open the encrypted volume on the Nitrokey Storage. +The user PIN that is required to open the volume is queried using +\fBpinentry\fR(1) and cached by \fBgpg\-agent\fR(1). +.TP +\fBnitrocli encrypted close +Close the encrypted volume on the Nitrokey Storage. +.TP +\fBnitrocli hidden create \fIslot\fR \fIstart\fR \fIend\fR +Create a new hidden volume inside the encrypted volume. \fIslot\fR must indicate +one of the four available slots. \fIstart\fR and \fIend\fR represent, +respectively, the start and end position of the hidden volume inside the +encrypted volume, as a percentage of the encrypted volume's size. +This command requires a password which is later used to look up the hidden +volume to open. Unlike a PIN, this password is not cached by \fBgpg\-agent\fR(1). +.TP +\fBnitrocli hidden open +Open a hidden volume. The volume to open is determined based on the password +entered, which must have a minimum of six characters. Only one hidden volume can +be active at any point in time and previously opened volumes will be +automatically closed. Similarly, the encrypted volume will be closed if it was +open. +.TP +\fBnitrocli hidden close +Close a hidden volume. + +.SS One-time passwords +The Nitrokey Pro and the Nitrokey Storage support the generation of one-time +passwords using the HOTP algorithm according to RFC 4226 or the TOTP algorithm +according to RFC 6238. +The required data \(en a name and the secret \(en is stored in slots. +Currently, the Nitrokey devices provide three HOTP slots and 15 TOTP slots. +The slots are numbered per algorithm starting at zero. +.P +The TOTP algorithm is a modified version of the HOTP algorithm that also uses +the current time. +Therefore, the Nitrokey clock must be synchronized with the clock of the +application that requests the one-time password. +.TP +\fBnitrocli otp get \fIslot \fR[\fB\-a\fR|\fB\-\-algorithm \fIalgorithm\fR] \ +\fB[\-t\fR|\fB\-\-time \fItime\fR] +Generate a one-time password. +\fIslot\fR is the number of the slot to generate the password from. +\fIalgorithm\fR is the OTP algorithm to use. +Possible values are \fBhotp\fR for the HOTP algorithm according to RFC 4226 and +\fBtotp\fR for the TOTP algorithm according to RFC 6238 (default). +Per default, this commands sets the Nitrokey's time to the system time if the +TOTP algorithm is selected. +If \fB\-\-time\fR is set, it is set to \fItime\fR instead, which must be a Unix +timestamp (i.e., the number of seconds since 1970-01-01 00:00:00 UTC). +This command might require the user PIN (see the Configuration section). +.TP +\fBnitrocli otp set \fIslot name secret \ +\fR[\fB\-a\fR|\fB\-\-algorithm \fIalgorithm\fR] \ +[\fB\-d\fR|\fB\-\-digits \fIdigits\fR] [\fB\-c\fR|\fB\-\-counter \fIcounter\fR] \ +[\fB\-t\fR|\fB\-\-time-window \fItime-window\fR] \ +[\fB-f\fR|\fB\-\-format ascii\fR|\fBbase32\fR|\fBhex\fR] +Configure a one-time password slot. +\fIslot\fR is the number of the slot to configure. +\fIname\fR is the name of the slot (may not be empty). +\fIsecret\fR is the secret value to store in that slot. + +The \fB\-\-format\fR option specifies the format of the secret. +If it is set to \fBascii\fR, each character of the given secret is interpreted +as the ASCII code of one byte. +If it is set to \fBbase32\fR, the secret is interpreted as a base32 string +according to RFC 4648. +If it is set to \fBhex\fR, every two characters are interpreted as the +hexadecimal value of one byte. +The default value is \fBhex\fR. + +\fIalgorithm\fR is the OTP algorithm to use. +Possible values are \fBhotp\fR for the HOTP algorithm according to RFC 4226 and +\fBtotp\fR for the TOTP algorithm according to RFC 6238 (default). +\fIdigits\fR is the number of digits the one-time password should have. +Allowed values are 6 and 8 (default: 6). +\fIcounter\fR is the initial counter if the HOTP algorithm is used (default: 0). +\fItime window\fR is the time window used with TOTP in seconds (default: 30). +.TP +\fBnitrocli otp clear \fIslot \fR[\fB\-a\fR|\fB\-\-algorithm \fIalgorithm\fR] +Delete the name and the secret stored in a one-time password slot. +\fIslot\fR is the number of the slot to clear. +\fIalgorithm\fR is the OTP algorithm to use. +Possible values are \fBhotp\fR for the HOTP algorithm according to RFC 4226 and +\fBtotp\fR for the TOTP algorithm according to RFC 6238 (default). +.TP +\fBnitrocli otp status \fR[\fB\-a\fR|\fB\-\-all\fR] +List all OTP slots. +If \fB\-\-all\fR is not set, empty slots are ignored. + +.SS Configuration +Nitrokey devices have four configuration settings: the numlock, capslock and +scrollock keys can be mapped to an HOTP slot, and OTP generation can be set to +require the user PIN. +.TP +\fBnitrocli config get\fR +Print the current configuration. +.TP +\fBnitrocli config set \fR\ +[[\fB\-n\fR|\fB\-\-numlock \fIslot\fR] | [\fB\-N\fR|\fB\-\-no\-numlock\fR]] \ +[[\fB\-c\fR|\fB\-\-capslock \fIslot\fR] | [\fB\-C\fR|\fB\-\-no\-capslock\fR]] \ +[[\fB\-s\fR|\fB\-\-scrollock \fIslot\fR] | [\fB\-S\fR|\fB\-\-no\-scrollock\fR]] \ +[[\fB\-o\fR|\fB\-\-otp\-pin\fR] | [\fB\-O\fR|\fB\-\-no\-otp\-pin\fR]] +Update the Nitrokey configuration. +This command requires the admin PIN. + +With the \fB\-\-numlock\fR, \fB\-\-capslock\fR and \fB\-\-scrollock\fR options, +the respective bindings can be set. +\fIslot\fR is the number of the HOTP slot to bind the key to. +If \fB\-\-no\-numlock\fR, \fB\-\-no\-capslock\fR or \fB\-\-no\-scrollock\fR is +set, the respective binding is disabled. +The two corresponding options are mutually exclusive. + +If \fB\-\-otp\-pin\fR is set, the user PIN will be required to generate one-time +passwords using the \fBotp get\fR command. +If \fB\-\-no\-otp\-pin\fR is set, OTP generation can be performed without PIN. +These two options are mutually exclusive. + +.SS Password safe +The Nitrokey Pro and the Nitrokey Storage provide a password safe (PWS) with 20 +slots. +In each of these slots you can store a name, a login, and a password. +The PWS is not encrypted, but it is protected with the user PIN by the firmware. +Once the PWS is unlocked by one of the commands listed below, it can be +accessed without authentication. +You can use the \fBlock\fR command to lock the password safe. +.TP +\fBnitrocli pws get \fIslot \fR[\fB\-n\fR|\fB\-\-name\fR] \ +[\fB\-l\fR|\fB\-\-login\fR] \ +[\fB\-p\fR|\fB\-\-password\fR] \ +[\fB\-q\fR|\fB\-\-quiet\fR] +Print the content of one PWS slot. +\fIslot\fR is the number of the slot. +Per default, this command prints the name, the login and the password (in that +order). +If one or more of the options \fB\-\-name\fR, \fB\-\-login\fR, and +\fB\-\-password\fR are set, only the selected fields are printed. +The order of the fields never changes. + +The fields are printed together with a label. +Use the \fB\-\-quiet\fR option to suppress the labels and to only output the +values stored in the PWS slot. +.TP +\fBnitrocli pws set \fIslot name login password\fR +Set the content of a PWS slot. +\fIslot\fR is the number of the slot to write. +\fIname\fR, \fIlogin\fR, and \fIpassword\fR represent the data to write to the +slot. +.TP +\fBnitrocli pws clear \fIslot\fR +Delete the data stored in a PWS slot. +\fIslot\fR is the number of the slot clear. +.TP +\fBnitrocli pws status \fR[\fB\-a\fR|\fB\-\-all\fR] +List all PWS slots. +If \fB\-\-all\fR is not set, empty slots are ignored. + +.SS PINs +Nitrokey devices have two PINs: the user PIN and the admin PIN. The user +PIN must have at least six, the admin PIN at least eight characters. The +user PIN is required for commands such as \fBotp get\fR (depending on +the configuration) and for all \fBpws\fR commands. +The admin PIN is usually required to change the device configuration. +.P +Each PIN has a retry counter that is decreased with every wrong PIN entry and +reset if the PIN was entered correctly. +The initial retry counter is three. +If the retry counter for the user PIN is zero, you can use the +\fBpin unblock\fR command to unblock and reset the user PIN. +If the retry counter for the admin PIN is zero, you have to perform a factory +reset using the \fBreset\fR command or \fBgpg\fR(1). +Use the \fBstatus\fR command to check the retry counters. +.TP +.B nitrocli pin clear +Clear the PINs cached by the other commands. Note that cached PINs are +associated with the device they belong to and the \fBclear\fR command will only +clear the PIN for the currently used device, not all others. +.TP +\fBnitrocli pin set \fItype\fR +Change a PIN. +\fItype\fR is the type of the PIN that will be changed: \fBadmin\fR to change +the admin PIN or \fBuser\fR to change the user PIN. +This command only works if the retry counter for the PIN type is at least one. +(Use the \fBstatus\fR command to check the retry counters.) +.TP +.B nitrocli pin unblock +Unblock and reset the user PIN. +This command requires the admin PIN. +The admin PIN cannot be unblocked. +This operation is equivalent to the unblock PIN option provided by \fBgpg\fR(1) +(using the \fB\-\-change\-pin\fR option). + +.SH ENVIRONMENT +The program honors a set of environment variables that can be used to +suppress interactive PIN entry through \fBpinentry\fR(1). The following +variables are recognized: +.TP +.B NITROCLI_ADMIN_PIN +The admin PIN to use. +.TP +.B NITROCLI_USER_PIN +The user PIN to use. +.TP +.B NITROCLI_NEW_ADMIN_PIN +The new admin PIN to set. This variable is only used by the \fBpin set\fR +command for the \fBadmin\fR type. +.TP +.B NITROCLI_NEW_USER_PIN +The new user PIN to set. This variable is only used by the \fBpin set\fR command +for the \fBuser\fR type. +.TP +.B NITROCLI_PASSWORD +A password used by commands that require one (e.g., \fBhidden open\fR). +.TP +.B NITROCLI_NO_CACHE +If this variable is present in the environment, do not cache any inquired +secrets using \fBgpg\-agent\fR(1) but ask for them each time they are needed. +Note that this variable does not cause any cached secrets to be cleared. If a +secret is already in the cache it will be ignored, but left otherwise untouched. +Use the \fBpin clear\fR command to clear secrets from the cache. + +.SH EXAMPLES +.SS Storage +Create a hidden volume in the first available slot, starting at half the size of +the encrypted volume (i.e., 50%) and stretching all the way to its end (100%): + $ \fBnitrocli hidden create 0 50 100\fR + +.SS One-time passwords +Configure a one-time password slot with a hexadecimal secret representation: + $ \fBnitrocli otp set 0 test\-rfc4226 3132333435363738393031323334353637383930 \-\-algorithm hotp\fR + $ \fBnitrocli otp set 1 test\-foobar 666F6F626172 \-\-algorithm hotp\fR + $ \fBnitrocli otp set 0 test\-rfc6238 3132333435363738393031323334353637383930 \-\-algorithm totp \-\-digits 8\fR +.P +Configure a one-time password slot with an ASCII secret representation: + $ \fBnitrocli otp set 0 test\-rfc4226 12345678901234567890 \-\-format ascii \-\-algorithm hotp\fR + $ \fBnitrocli otp set 1 test\-foobar foobar \-\-format ascii \-\-algorithm hotp\fR + $ \fBnitrocli otp set 0 test\-rfc6238 12345678901234567890 \-\-format ascii \-\-algorithm totp \-\-digits 8\fR +.P +Configure a one-time password slot with a base32 secret representation: + $ \fBnitrocli otp set 0 test\-rfc4226 gezdgnbvgy3tqojqgezdgnbvgy3tqojq \-\-format base32 \-\-algorithm hotp\fR + $ \fBnitrocli otp set 1 test\-foobar mzxw6ytboi====== \-\-format base32 \-\-algorithm hotp\fR + $ \fBnitrocli otp set 0 test\-rfc6238 gezdgnbvgy3tqojqgezdgnbvgy3tqojq \-\-format base32 \-\-algorithm totp \-\-digits 8\fR +.P +Generate a one-time password: + $ \fBnitrocli otp get 0 \-\-algorithm hotp\fR + 755224 + $ \fBnitrocli otp get 0 \-\-algorithm totp \-\-time 1234567890\fR + 89005924 +.P +Clear a one-time password slot: + $ \fBnitrocli otp clear 0 \-\-algorithm hotp\fR + +.SS Configuration +Query the configuration: + $ \fBnitrocli config get\fR + Config: + numlock binding: not set + capslock binding: not set + scrollock binding: not set + require user PIN for OTP: true +.P +Change the configuration: + $ \fBnitrocli config set \-\-otp\-pin\fR + +.SS Password safe +Configure a PWS slot: + $ \fBnitrocli pws set 0 example.org john.doe passw0rd\fR + +Get the data from a slot: + $ \fBnitrocli pws get 0\fR + name: example.org + login: john.doe + password: passw0rd + +Copy the password to the clipboard (requires \fBxclip\fR(1)). + $ \fBnitrocli pws get 0 \-\-password \-\-quiet | xclip \-in\fR + +Query the PWS slots: + $ \fB nitrocli pws status\fR + slot name + 0 example.org diff --git a/doc/nitrocli.1.pdf b/doc/nitrocli.1.pdf new file mode 100644 index 0000000..cd15a66 Binary files /dev/null and b/doc/nitrocli.1.pdf differ diff --git a/doc/packaging.md b/doc/packaging.md new file mode 100644 index 0000000..5ff4089 --- /dev/null +++ b/doc/packaging.md @@ -0,0 +1,64 @@ +How to package nitrocli +======================= + +This document describes how to update the packaged versions of nitrocli. + +Arch Linux +---------- + +1. Clone the Git repository at ssh://aur@aur.archlinux.org/nitrocli.git. +2. Edit the `PKGBUILD` file: + - Update the `pkgver` variable to the current nitrocli version. + - If the `pkgrel` variable is not 1, set it to 1. + - Update the SHA512 hash in the `sha512sums` variable for the new tarball. +3. Update the `.SRCINFO` file by running `makepkg --printsrcinfo > .SRCINFO`. +4. Verify that the package builds successfully by running `makepkg`. +5. Verify that the package was built as expected by running `pacman -Qlp $f` + and `pacman -Qip $f`, where `$f` is `nitrocli-$pkgver.pkg.tar.gz`. +6. Check the package for errors by running `namcap PKGBUILD` and `namcap + nitrocli-$pkgver.pkg.tar.gz`. +7. Add, commit, and push your changes to publish them in the AUR. + +If you have to release a new package version without a new nitrocli version, +do not change the `pkgver` variable and instead increment the `pkgrel` +variable. + +For more information, see the [Arch User Repository][] page in the Arch Wiki. + +Debian +------ + +1. Clone or fork the Git repository at + https://salsa.debian.org/rust-team/debcargo-conf. +2. Execute `./update.sh nitrocli`. +3. Check and, if necessary, update the Debian changelog in the file + `src/nitrocli/debian/changelog`. +4. Verify that the package builds successfully by running `./build.sh nitrocli` + in the `build` directory. (This requires an `sbuild` environment as + described in the `README.rst` file.) +5. Inspect the generated package by running `dpkg-deb --info` and `dpkg-deb + --contents` on it. +6. If you have push access to the repository, create the + `src/nitrocli/debian/RFS` file to indicate that `nitrocli` can be updated. +7. Add and commit your changes. If you have push access, push them. + Otherwise create a merge request and indicate that `nitrocli` is ready for + upload in its description. + +For more information, see the [Teams/RustPackaging][] page in the Debian Wiki +and the [README.rst file][] in the debcargo-conf repository. + +For detailed information on the status of the Debian package, check the [Debian +Package Tracker][]. + +Ubuntu +------ + +The `nitrocli` package for Ubuntu is automatically generated from the Debian +package. For detailed information on the status of the Ubuntu package, check +[Launchpad][]. + +[Arch User Repository]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[Teams/RustPackaging]: https://wiki.debian.org/Teams/RustPackaging +[README.rst file]: https://salsa.debian.org/rust-team/debcargo-conf/blob/master/README.rst +[Debian Package Tracker]: https://tracker.debian.org/pkg/rust-nitrocli +[Launchpad]: https://launchpad.net/ubuntu/+source/rust-nitrocli diff --git a/nitrocli/.gitignore b/nitrocli/.gitignore deleted file mode 100644 index c6262ea..0000000 --- a/nitrocli/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target -*.swp diff --git a/nitrocli/CHANGELOG.md b/nitrocli/CHANGELOG.md deleted file mode 100644 index 9147ec5..0000000 --- a/nitrocli/CHANGELOG.md +++ /dev/null @@ -1,147 +0,0 @@ -Unreleased ----------- -- Removed vendored dependencies - - -0.3.1 ------ -- Added note about interaction with GnuPG to `README` file -- Bumped `nitrokey` dependency to `0.4.0` - - Bumped `nitrokey-sys` dependency to `3.5.0` - - Added `lazy_static` dependency in version `1.4.0` - - Added `cfg-if` dependency in version `0.1.10` - - Added `getrandom` dependency in version `0.1.13` - - -0.3.0 ------ -- Added `unencrypted` command with `set` subcommand for changing the - unencrypted volume's read-write mode -- Changed `storage hidden` subcommand to `hidden` top-level command -- Renamed `storage` command to `encrypted` -- Removed `storage status` subcommand - - Moved its output into `status` command -- Removed previously deprecated `--ascii` option from `otp set` command -- Fixed wrong hexadecimal conversion used in `otp set` command -- Bumped `nitrokey` dependency to `0.3.5` -- Bumped `libc` dependency to `0.2.66` -- Bumped `cc` dependency to `1.0.48` - - -0.2.4 ------ -- Added the `reset` command to perform a factory reset -- Added the `-V`/`--version` option to print the program's version -- Check the status of a PWS slot before accessing it in `pws get` -- Added `NITROCLI_NO_CACHE` environment variable to bypass caching of - secrets -- Clear cached PIN entry as part of `pin set` command to prevent - spurious authentication failures -- Bumped `libc` dependency to `0.2.57` -- Bumped `cc` dependency to `1.0.37` - - -0.2.3 ------ -- Added the `storage hidden` subcommand for working with hidden volumes -- Store cached PINs on a per-device basis to better support multi-device - scenarios -- Further decreased binary size by using system allocator -- Bumped `nitrokey` dependency to `0.3.4` - - Bumped `rand` dependency to `0.6.4` - - Removed `rustc_version`, `semver`, and `semver-parser` dependencies -- Bumped `nitrokey-sys` dependency to `3.4.3` -- Bumped `libc` dependency to `0.2.47` - - -0.2.2 ------ -- Added the `-v`/`--verbose` option to control libnitrokey log level -- Added the `-m`/`--model` option to restrict connections to a device - model -- Added the `-f`/`--format` option for the `otp set` subcommand to - choose the secret format - - Deprecated the `--ascii` option -- Honor `NITROCLI_ADMIN_PIN` and `NITROCLI_USER_PIN` as well as - `NITROCLI_NEW_ADMIN_PIN` and `NITROCLI_NEW_USER_PIN` environment - variables for non-interactive PIN supply -- Format `nitrokey` reported errors in more user-friendly format -- Bumped `nitrokey` dependency to `0.3.1` - - -0.2.1 ------ -- Added the `pws` command for accessing the password safe -- Added the `lock` command for locking the Nitrokey device -- Adjusted release build compile options to optimize binary for size -- Bumped `nitrokey` dependency to `0.2.3` - - Bumped `rand` dependency to `0.6.1` - - Added `rustc_version` version `0.2.3`, `semver` version `0.9.0`, and - `semver-parser` version `0.7.0` as indirect dependencies -- Bumped `cc` dependency to `1.0.28` - - -0.2.0 ------ -- Use the `nitrokey` crate for the `open`, `close`, and `status` - commands instead of directly communicating with the Nitrokey device - - Added `nitrokey` version `0.2.1` as a direct dependency and - `nitrokey-sys` version `3.4.1` as well as `rand` version `0.4.3` as - indirect dependencies - - Removed the `hid`, `hidapi-sys` and `pkg-config` dependencies -- Added the `otp` command for working with one-time passwords -- Added the `config` command for reading and writing the device configuration -- Added the `pin` command for managing PINs - - Renamed the `clear` command to `pin clear` -- Moved `open` and `close` commands as subcommands into newly introduced - `storage` command - - Moved printing of storage related information from `status` command - into new `storage status` subcommand -- Made `status` command work with Nitrokey Pro devices -- Enabled CI pipeline comprising code style conformance checks, linting, - and building of the project -- Added badges indicating pipeline status, current `crates.io` published - version of the crate, and minimum version of `rustc` required -- Fixed wrong messages in the pinentry dialog that were caused by unescaped - spaces in a string -- Use the `argparse` crate to parse the command-line arguments - - Added `argparse` dependency in version `0.2.2` - - -0.1.3 ------ -- Show PIN related errors through `pinentry` native reporting mechanism - instead of emitting them to `stdout` -- Added a `man` page (`nitrocli(1)`) for the program to the repository -- Adjusted program to use Rust Edition 2018 -- Enabled more lints -- Applied a couple of `clippy` reported suggestions -- Added categories to `Cargo.toml` -- Changed dependency version requirements to be less strict (only up to - the minor version and not the patch level) -- Bumped `pkg-config` dependency to `0.3.14` -- Bumped `libc` dependency to `0.2.45` -- Bumped `cc` dependency to `1.0.25` - - -0.1.2 ------ -- Replaced deprecated `gcc` dependency with `cc` and bumped to `1.0.4` -- Bumped `hid` dependency to `0.4.1` -- Bumped `hidapi-sys` dependency to `0.1.4` -- Bumped `libc` dependency to `0.2.36` - - -0.1.1 ------ -- Fixed display of firmware version for `status` command -- Removed workaround for incorrect CRC checksum produced by the Nitrokey - Storage device - - The problem has been fixed upstream (`nitrokey-storage-firmware` - [issue #32](https://github.com/Nitrokey/nitrokey-storage-firmware/issues/32)) - - In order to be usable, a minimum firmware version of 0.47 is required - - -0.1.0 ------ -- Initial release diff --git a/nitrocli/Cargo.lock b/nitrocli/Cargo.lock deleted file mode 100644 index 3e914a4..0000000 --- a/nitrocli/Cargo.lock +++ /dev/null @@ -1,193 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "aho-corasick" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "argparse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "base32" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "cc" -version = "1.0.48" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "getrandom" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "libc" -version = "0.2.66" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "memchr" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "nitrocli" -version = "0.3.1" -dependencies = [ - "argparse 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "base32 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "nitrokey 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "nitrokey-test 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "nitrokey-test-state 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "nitrokey" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "nitrokey-sys 3.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "nitrokey-sys" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "nitrokey-test" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "nitrokey-test-state" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "proc-macro2" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "quote" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "regex" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", - "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "regex-syntax" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "syn" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "thread_local" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "unicode-xid" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "wasi" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[metadata] -"checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" -"checksum argparse 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47" -"checksum base32 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" -"checksum cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "f52a465a666ca3d838ebbf08b241383421412fe7ebb463527bba275526d89f76" -"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" -"checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407" -"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" -"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" -"checksum nitrokey 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a5f19c16e1530850b171e98ad1c95fe0af897916eb308e19fbfd3ee2dcda0abe" -"checksum nitrokey-sys 3.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "71c6052aeb37309317d25c8fec2801f271b96c5a15656f2573d8e78ba4124c49" -"checksum nitrokey-test 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f3da0c2cedaa512f79fbc3ed45143a52c76c5edcca88d0823b967ff11d05fe37" -"checksum nitrokey-test-state 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a59b732ed6d5212424ed31ec9649f05652bcbc38f45f2292b27a6044e7098803" -"checksum proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0319972dcae462681daf4da1adeeaa066e3ebd29c69be96c6abb1259d2ee2bcc" -"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" -"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -"checksum regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd" -"checksum regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716" -"checksum syn 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ddc157159e2a7df58cd67b1cace10b8ed256a404fb0070593f137d8ba6bef4de" -"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" -"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" -"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" diff --git a/nitrocli/Cargo.toml b/nitrocli/Cargo.toml deleted file mode 100644 index 63e6409..0000000 --- a/nitrocli/Cargo.toml +++ /dev/null @@ -1,64 +0,0 @@ -# Cargo.toml - -#/*************************************************************************** -# * Copyright (C) 2017-2020 Daniel Mueller (deso@posteo.net) * -# * * -# * This program is free software: you can redistribute it and/or modify * -# * it under the terms of the GNU General Public License as published by * -# * the Free Software Foundation, either version 3 of the License, or * -# * (at your option) any later version. * -# * * -# * This program is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU General Public License for more details. * -# * * -# * You should have received a copy of the GNU General Public License * -# * along with this program. If not, see . * -# ***************************************************************************/ - -[package] -name = "nitrocli" -version = "0.3.1" -edition = "2018" -authors = ["Daniel Mueller "] -license = "GPL-3.0-or-later" -homepage = "https://github.com/d-e-s-o/nitrocli" -repository = "https://github.com/d-e-s-o/nitrocli.git" -readme = "README.md" -categories = ["command-line-utilities", "authentication", "cryptography", "hardware-support"] -keywords = ["nitrokey", "nitrokey-storage", "nitrokey-pro", "cli", "usb"] -description = """ -A command line tool for interacting with Nitrokey devices. -""" -exclude = ["ci/*", "rustfmt.toml"] - -[badges] -gitlab = { repository = "d-e-s-o/nitrocli", branch = "master" } - -[profile.release] -opt-level = "z" -lto = true -codegen-units = 1 -incremental = false - -[dependencies.argparse] -version = "0.2.2" - -[dependencies.base32] -version = "0.4.0" - -[dependencies.libc] -version = "0.2" - -[dependencies.nitrokey] -version = "0.4.0" - -[dev-dependencies.nitrokey-test] -version = "0.3.1" - -[dev-dependencies.nitrokey-test-state] -version = "0.1" - -[dev-dependencies.regex] -version = "1" diff --git a/nitrocli/LICENSE b/nitrocli/LICENSE deleted file mode 100644 index ef7e7ef..0000000 --- a/nitrocli/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ -GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - {project} Copyright (C) {year} {fullname} - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/nitrocli/Makefile b/nitrocli/Makefile deleted file mode 100644 index e7f7da5..0000000 --- a/nitrocli/Makefile +++ /dev/null @@ -1,70 +0,0 @@ -# Makefile - -#/*************************************************************************** -# * Copyright (C) 2017-2019 Daniel Mueller (deso@posteo.net) * -# * * -# * This program is free software: you can redistribute it and/or modify * -# * it under the terms of the GNU General Public License as published by * -# * the Free Software Foundation, either version 3 of the License, or * -# * (at your option) any later version. * -# * * -# * This program is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU General Public License for more details. * -# * * -# * You should have received a copy of the GNU General Public License * -# * along with this program. If not, see . * -# ***************************************************************************/ - -SHELL := bash - -PS2PDF ?= ps2pdf - -NITROCLI_MAN := doc/nitrocli.1 -NITROCLI_PDF := $(addsuffix .pdf,$(NITROCLI_MAN)) - -.PHONY: doc -doc: $(NITROCLI_PDF) $(NITROCLI_HTML) - -# We assume and do not check existence of man, which, false, and echo -# commands. -$(NITROCLI_PDF): $(NITROCLI_MAN) - @which $(PS2PDF) &> /dev/null || \ - (echo "$(PS2PDF) command not found, unable to generate documentation"; false) - @man --local-file --troff $(<) | $(PS2PDF) - $(@) - -KEY ?= 0x952DD6F8F34D8B8E - -.PHONY: sign -sign: - @test -n "$(REL)" || \ - (echo "Please set REL environment variable to the release to verify (e.g., '0.2.1')."; false) - @mkdir -p pkg/ - wget --quiet "https://github.com/d-e-s-o/nitrocli/archive/v$(REL).zip" \ - -O "pkg/nitrocli-$(REL).zip" - @set -euo pipefail && DIR1=$$(mktemp -d) && DIR2=$$(mktemp -d) && \ - unzip -q pkg/nitrocli-$(REL).zip -d $${DIR1} && \ - git -C $$(git rev-parse --show-toplevel) archive --prefix=nitrocli-$(REL)/ v$(REL) | \ - tar -x -C $${DIR2} && \ - diff -u -r $${DIR1} $${DIR2} && \ - echo "Github zip archive verified successfully" && \ - (rm -r $${DIR1} && rm -r $${DIR2}) - wget --quiet "https://github.com/d-e-s-o/nitrocli/archive/v$(REL).tar.gz" \ - -O "pkg/nitrocli-$(REL).tar.gz" - @set -euo pipefail && DIR1=$$(mktemp -d) && DIR2=$$(mktemp -d) && \ - tar -xz -C $${DIR1} -f pkg/nitrocli-$(REL).tar.gz && \ - git -C $$(git rev-parse --show-toplevel) archive --prefix=nitrocli-$(REL)/ v$(REL) | \ - tar -x -C $${DIR2} && \ - diff -u -r $${DIR1} $${DIR2} && \ - echo "Github tarball verified successfully" && \ - (rm -r $${DIR1} && rm -r $${DIR2}) - @cd pkg && sha256sum nitrocli-$(REL).tar.gz nitrocli-$(REL).zip > nitrocli-$(REL).sha256.DIGEST - @gpg --sign --armor --detach-sign --default-key=$(KEY) --yes \ - --output pkg/nitrocli-$(REL).sha256.DIGEST.sig pkg/nitrocli-$(REL).sha256.DIGEST - @gpg --verify pkg/nitrocli-$(REL).sha256.DIGEST.sig - @cd pkg && sha256sum --check < nitrocli-$(REL).sha256.DIGEST - @echo "All checks successful. Please attach" - @echo " pkg/nitrocli-$(REL).sha256.DIGEST" - @echo " pkg/nitrocli-$(REL).sha256.DIGEST.sig" - @echo "to https://github.com/d-e-s-o/nitrocli/releases/tag/v$(REL)" diff --git a/nitrocli/README.md b/nitrocli/README.md deleted file mode 100644 index 0f01ac5..0000000 --- a/nitrocli/README.md +++ /dev/null @@ -1,167 +0,0 @@ -[![pipeline](https://gitlab.com/d-e-s-o/nitrocli/badges/master/pipeline.svg)](https://gitlab.com/d-e-s-o/nitrocli/commits/master) -[![crates.io](https://img.shields.io/crates/v/nitrocli.svg)](https://crates.io/crates/nitrocli) -[![rustc](https://img.shields.io/badge/rustc-1.35+-blue.svg)](https://blog.rust-lang.org/2019/05/23/Rust-1.35.0.html) - -nitrocli -======== - -- [Changelog](CHANGELOG.md) - -**nitrocli** is a program that provides a command line interface for -interaction with [Nitrokey Pro][nitrokey-pro] and [Nitrokey -Storage][nitrokey-storage] devices. - - -The following commands are currently supported: -- status: Report status information about the Nitrokey. -- lock: Lock the Nitrokey. -- config: Access the Nitrokey's configuration - - get: Read the current configuration. - - set: Change the configuration. -- encrypted: Work with the Nitrokey Storage's encrypted volume. - - open: Open the encrypted volume. The user PIN needs to be entered. - - close: Close the encrypted volume. -- hidden: Work with the Nitrokey Storage's hidden volume. - - create: Create a hidden volume. - - open: Open a hidden volume with a password. - - close: Close a hidden volume. -- otp: Access one-time passwords (OTP). - - get: Generate a one-time password. - - set: Set an OTP slot. - - status: List all OTP slots. - - clear: Delete an OTP slot. -- pin: Manage the Nitrokey's PINs. - - clear: Remove the user and admin PIN from gpg-agent's cache. - - set: Change the admin or the user PIN. - - unblock: Unblock and reset the user PIN. -- pws: Access the password safe (PWS). - - get: Query the data on a PWS slot. - - set: Set the data on a PWS slot. - - status: List all PWS slots. - - clear: Delete a PWS slot. -- unencrypted: Work with the Nitrokey Storage's unencrypted volume. - - set: Change the read-write mode of the unencrypted volume. - - -Usage ------ - -Usage is as simple as providing the name of the respective command as a -parameter (note that some commands are organized through subcommands, -which are required as well), e.g.: -```bash -# Open the nitrokey's encrypted volume. -$ nitrocli storage open - -$ nitrocli status -Status: - model: Storage - serial number: 0x00053141 - firmware version: 0.47 - user retry count: 3 - admin retry count: 3 - Storage: - SD card ID: 0x05dcad1d - firmware: unlocked - storage keys: created - volumes: - unencrypted: active - encrypted: active - hidden: inactive - -# Close it again. -$ nitrocli storage close -``` - -More examples, a more detailed explanation of the purpose, the potential -subcommands, as well as the parameters of each command are provided in -the [`man` page](doc/nitrocli.1.pdf). - - -Installation ------------- - -In addition to Rust itself and Cargo, its package management tool, the -following dependencies are required: -- **hidapi**: In order to provide USB access this library is used. -- **GnuPG**: The `gpg-connect-agent` program allows the user to enter - PINs. - -#### Via Packages -Packages are available for: -- Arch Linux: [`nitrocli`][nitrocli-arch] in the Arch User Repository -- Debian: [`nitrocli`][nitrocli-debian] (since Debian Buster) -- Gentoo Linux: [`app-crypt/nitrocli`][nitrocli-gentoo] ebuild -- Ubuntu: [`nitrocli`][nitrocli-ubuntu] (since Ubuntu 19.04) - -#### From Crates.io -**nitrocli** is [published][nitrocli-cratesio] on crates.io and can -directly be installed from there: -```bash -$ cargo install nitrocli --root=$PWD/nitrocli -``` - -#### From Source -After cloning the repository and changing into the `nitrocli` subfolder, -the build is as simple as running: -```bash -$ cargo build --release -``` - -It is recommended that the resulting executable be installed in a -directory accessible via the `PATH` environment variable. - - -Known Problems --------------- - -- Due to a problem with the default `hidapi` version on macOS, users are - advised to build and install [`libnitrokey`][] from source and then - set the `USE_SYSTEM_LIBNITROKEY` environment variable when building - `nitrocli` using one of the methods described above. -- `nitrocli` cannot connect to a Nitrokey device that is currently being - accessed by `nitrokey-app` ([upstream issue][libnitrokey#32]). To - prevent this problem, quit `nitrokey-app` before using `nitrocli`. -- Applications using the Nitrokey device (such as `nitrocli` or - `nitrokey-app`) cannot easily share access with an instance of GnuPG - running shortly afterwards ([upstream issue][libnitrokey#137]). - - -Contributing ------------- - -Contributions are generally welcome. Please follow the guidelines -outlined in [CONTRIBUTING.md](doc/CONTRIBUTING.md). - - -Acknowledgments ---------------- - -Robin Krahl ([@robinkrahl](https://github.com/robinkrahl)) has been -a crucial help for the development of **nitrocli**. - -The [Nitrokey UG][nitrokey-ug] has generously provided the necessary -hardware for developing and testing the program. - - -License -------- -**nitrocli** is made available under the terms of the -[GPLv3][gplv3-tldr]. - -See the [LICENSE](LICENSE) file that accompanies this distribution for -the full text of the license. - - -[`libnitrokey`]: https://github.com/nitrokey/libnitrokey -[nitrokey-ug]: https://www.nitrokey.com -[nitrokey-pro]: https://shop.nitrokey.com/shop/product/nitrokey-pro-2-3 -[nitrokey-storage]: https://shop.nitrokey.com/shop/product/nitrokey-storage-2-16gb-23 -[nitrocli-arch]: https://aur.archlinux.org/packages/nitrocli -[nitrocli-cratesio]: https://crates.io/crates/nitrocli -[nitrocli-debian]: https://packages.debian.org/stable/nitrocli -[nitrocli-gentoo]: https://packages.gentoo.org/packages/app-crypt/nitrocli -[nitrocli-ubuntu]: https://packages.ubuntu.com/search?keywords=nitrocli -[gplv3-tldr]: https://tldrlegal.com/license/gnu-general-public-license-v3-(gpl-3) -[libnitrokey#32]: https://github.com/Nitrokey/libnitrokey/issues/32 -[libnitrokey#137]: https://github.com/Nitrokey/libnitrokey/issues/137 diff --git a/nitrocli/ci/gitlab-ci.yml b/nitrocli/ci/gitlab-ci.yml deleted file mode 100644 index 29bf59d..0000000 --- a/nitrocli/ci/gitlab-ci.yml +++ /dev/null @@ -1,36 +0,0 @@ -# The documentation for the contents of this file can be found at: -# https://docs.gitlab.com/ce/ci/yaml/README.html - -# Official language image. Look for the different tagged releases at: -# https://hub.docker.com/r/library/rust/tags/ -# The recipe for this docker image can be found at: -# https://github.com/rust-lang/docker-rust/blob/29bf41a2cc4fb8d3f588cf51eb6a8ba883808c4b/1.35.0/stretch/Dockerfile -image: "rust:1.35.0" - -build-test:cargo: - script: - - apt-get update - - apt-get install --assume-yes libudev-dev libhidapi-dev - - rustc --version && cargo --version - - cd nitrocli - - cargo build --all --verbose - - cargo test --all --verbose - -lint:clippy: - script: - - rustup component add clippy - # First check and build everything but be very permissive. Then clean - # only the nitrocli package artifacts. Lastly check once more, but - # with warnings turned to errors. This last run will only recheck - # nitrocli (everything else is still up-to-date). That procedure is - # necessary because consumed dependencies may emit errors otherwise. - - cd nitrocli - - cargo clippy --all-targets --all-features -- -A clippy::all - - cargo clean --package=nitrocli - - cargo clippy --all-targets --all-features -- -D warnings - -format:rustfmt: - script: - - rustup component add rustfmt - - cd nitrocli - - cargo fmt --all -- --check diff --git a/nitrocli/doc/CONTRIBUTING.md b/nitrocli/doc/CONTRIBUTING.md deleted file mode 100644 index 3ebdfce..0000000 --- a/nitrocli/doc/CONTRIBUTING.md +++ /dev/null @@ -1,23 +0,0 @@ -The following rules generally apply for pull requests and code changes: - -**Submit Pull Requests to the `devel` branch** - -The `devel` branch is where experimental features reside. After some -soak time they may be ported over to `master` and a release will be cut -that includes them. - -**Keep documentation up-to-date** - -Please make an effort to keep the documentation up-to-date to the extent -possible and necessary for the change at hand. That includes adjusting -the [README](../README.md) and [`man` page](nitrocli.1) as well as -regenerating the PDF rendered version of the latter by running `make -doc`. - -**Blend with existing patterns and style** - -To keep the code as consistent as possible, please try not to diverge -from the existing style used in a file. Specifically for Rust source -code, use [`rustfmt`](https://github.com/rust-lang/rustfmt) and -[`clippy`](https://github.com/rust-lang/rust-clippy) to achieve a -minimum level of consistency and prevent known bugs, respectively. diff --git a/nitrocli/doc/nitrocli.1 b/nitrocli/doc/nitrocli.1 deleted file mode 100644 index 75f5960..0000000 --- a/nitrocli/doc/nitrocli.1 +++ /dev/null @@ -1,355 +0,0 @@ -.TH NITROCLI 1 2019-06-08 -.SH NAME -nitrocli \- access Nitrokey devices -.SH SYNOPSIS -.B nitrocli -[\fB\-m\fR|\fB\-\-model pro\fR|\fBstorage\fR] \fR[\fB\-v\fR|\fB\-\-verbose\fR] -[\fB\-V\fR|\fB\-\-version\fR] -\fIcommand\fR -[\fIarguments\fR] -.SH DESCRIPTION -\fBnitrocli\fR provides access to Nitrokey devices. -It supports the Nitrokey Pro and the Nitrokey Storage. -It can be used to access the encrypted volume, the one-time password generator, -and the password safe. -.SH OPTIONS -.TP -\fB\-m\fR, \fB\-\-model pro\fR|\fBstorage\fR -Restrict connections to the given device model. -If this option is not set, nitrocli will connect to any connected Nitrokey Pro -or Nitrokey Storage device. -.TP -\fB\-v\fR, \fB\-\-verbose\fR -Enable additional logging and control its verbosity. Logging enabled through -this option will appear on the standard error stream. This option can be -supplied multiple times. A single occurrence will show additional warnings. -Commands sent to the device will be shown when supplied three times and full -device communication is available with four occurrences. Supplying this option -five times enables the highest verbosity. -.TP -\fB\-V\fR, \fB\-\-version\fR -Print the nitrocli version and exit. -.SH COMMANDS -.SS General -.TP -.B nitrocli status -Print the status of the connected Nitrokey device, including the stick serial -number, the firmware version, and the PIN retry count. If the device is a -Nitrokey Storage, also print storage related information including the SD card -serial number, the encryption status, and the status of the volumes. -.TP -.B nitrocli lock -Lock the Nitrokey. -This command locks the password safe (see the Password safe section). On the -Nitrokey Storage, it will also close any active encrypted or hidden volumes (see -the Storage section). -.TP -.B nitrocli reset -Perform a factory reset on the Nitrokey. -This command performs a factory reset on the OpenPGP smart card, clears the -flash storage and builds a new AES key. -The user PIN is reset to 123456, the admin PIN to 12345678. - -This command requires the admin PIN. -To avoid accidental calls of this command, the user has to enter the PIN even -if it has been cached. - -.SS Storage -The Nitrokey Storage comes with a storage area. This area is comprised of an -\fIunencrypted\fR region and an \fIencrypted\fR one of fixed sizes, each made -available to the user in the form of block devices. The encrypted region can -optionally further be overlayed with up to four \fIhidden\fR volumes. Because of -this overlay (which is required to achieve plausible deniability of the -existence of hidden volumes), the burden of ensuring that data on the encrypted -volume does not overlap with data on one of the hidden volumes is on the user. -.TP -\fBnitrocli unencrypted set \fImode\fR -Change the read-write mode of the volume. -\fImode\fR is the type of the mode to change to: \fBread-write\fR to make the -volume readable and writable or \fBread-only\fR to make it only readable. -This command requires the admin PIN. - -Note that this command requires firmware version 0.51 or higher. Earlier -versions are not supported. -.TP -\fBnitrocli encrypted open -Open the encrypted volume on the Nitrokey Storage. -The user PIN that is required to open the volume is queried using -\fBpinentry\fR(1) and cached by \fBgpg\-agent\fR(1). -.TP -\fBnitrocli encrypted close -Close the encrypted volume on the Nitrokey Storage. -.TP -\fBnitrocli hidden create \fIslot\fR \fIstart\fR \fIend\fR -Create a new hidden volume inside the encrypted volume. \fIslot\fR must indicate -one of the four available slots. \fIstart\fR and \fIend\fR represent, -respectively, the start and end position of the hidden volume inside the -encrypted volume, as a percentage of the encrypted volume's size. -This command requires a password which is later used to look up the hidden -volume to open. Unlike a PIN, this password is not cached by \fBgpg\-agent\fR(1). -.TP -\fBnitrocli hidden open -Open a hidden volume. The volume to open is determined based on the password -entered, which must have a minimum of six characters. Only one hidden volume can -be active at any point in time and previously opened volumes will be -automatically closed. Similarly, the encrypted volume will be closed if it was -open. -.TP -\fBnitrocli hidden close -Close a hidden volume. - -.SS One-time passwords -The Nitrokey Pro and the Nitrokey Storage support the generation of one-time -passwords using the HOTP algorithm according to RFC 4226 or the TOTP algorithm -according to RFC 6238. -The required data \(en a name and the secret \(en is stored in slots. -Currently, the Nitrokey devices provide three HOTP slots and 15 TOTP slots. -The slots are numbered per algorithm starting at zero. -.P -The TOTP algorithm is a modified version of the HOTP algorithm that also uses -the current time. -Therefore, the Nitrokey clock must be synchronized with the clock of the -application that requests the one-time password. -.TP -\fBnitrocli otp get \fIslot \fR[\fB\-a\fR|\fB\-\-algorithm \fIalgorithm\fR] \ -\fB[\-t\fR|\fB\-\-time \fItime\fR] -Generate a one-time password. -\fIslot\fR is the number of the slot to generate the password from. -\fIalgorithm\fR is the OTP algorithm to use. -Possible values are \fBhotp\fR for the HOTP algorithm according to RFC 4226 and -\fBtotp\fR for the TOTP algorithm according to RFC 6238 (default). -Per default, this commands sets the Nitrokey's time to the system time if the -TOTP algorithm is selected. -If \fB\-\-time\fR is set, it is set to \fItime\fR instead, which must be a Unix -timestamp (i.e., the number of seconds since 1970-01-01 00:00:00 UTC). -This command might require the user PIN (see the Configuration section). -.TP -\fBnitrocli otp set \fIslot name secret \ -\fR[\fB\-a\fR|\fB\-\-algorithm \fIalgorithm\fR] \ -[\fB\-d\fR|\fB\-\-digits \fIdigits\fR] [\fB\-c\fR|\fB\-\-counter \fIcounter\fR] \ -[\fB\-t\fR|\fB\-\-time-window \fItime-window\fR] \ -[\fB-f\fR|\fB\-\-format ascii\fR|\fBbase32\fR|\fBhex\fR] -Configure a one-time password slot. -\fIslot\fR is the number of the slot to configure. -\fIname\fR is the name of the slot (may not be empty). -\fIsecret\fR is the secret value to store in that slot. - -The \fB\-\-format\fR option specifies the format of the secret. -If it is set to \fBascii\fR, each character of the given secret is interpreted -as the ASCII code of one byte. -If it is set to \fBbase32\fR, the secret is interpreted as a base32 string -according to RFC 4648. -If it is set to \fBhex\fR, every two characters are interpreted as the -hexadecimal value of one byte. -The default value is \fBhex\fR. - -\fIalgorithm\fR is the OTP algorithm to use. -Possible values are \fBhotp\fR for the HOTP algorithm according to RFC 4226 and -\fBtotp\fR for the TOTP algorithm according to RFC 6238 (default). -\fIdigits\fR is the number of digits the one-time password should have. -Allowed values are 6 and 8 (default: 6). -\fIcounter\fR is the initial counter if the HOTP algorithm is used (default: 0). -\fItime window\fR is the time window used with TOTP in seconds (default: 30). -.TP -\fBnitrocli otp clear \fIslot \fR[\fB\-a\fR|\fB\-\-algorithm \fIalgorithm\fR] -Delete the name and the secret stored in a one-time password slot. -\fIslot\fR is the number of the slot to clear. -\fIalgorithm\fR is the OTP algorithm to use. -Possible values are \fBhotp\fR for the HOTP algorithm according to RFC 4226 and -\fBtotp\fR for the TOTP algorithm according to RFC 6238 (default). -.TP -\fBnitrocli otp status \fR[\fB\-a\fR|\fB\-\-all\fR] -List all OTP slots. -If \fB\-\-all\fR is not set, empty slots are ignored. - -.SS Configuration -Nitrokey devices have four configuration settings: the numlock, capslock and -scrollock keys can be mapped to an HOTP slot, and OTP generation can be set to -require the user PIN. -.TP -\fBnitrocli config get\fR -Print the current configuration. -.TP -\fBnitrocli config set \fR\ -[[\fB\-n\fR|\fB\-\-numlock \fIslot\fR] | [\fB\-N\fR|\fB\-\-no\-numlock\fR]] \ -[[\fB\-c\fR|\fB\-\-capslock \fIslot\fR] | [\fB\-C\fR|\fB\-\-no\-capslock\fR]] \ -[[\fB\-s\fR|\fB\-\-scrollock \fIslot\fR] | [\fB\-S\fR|\fB\-\-no\-scrollock\fR]] \ -[[\fB\-o\fR|\fB\-\-otp\-pin\fR] | [\fB\-O\fR|\fB\-\-no\-otp\-pin\fR]] -Update the Nitrokey configuration. -This command requires the admin PIN. - -With the \fB\-\-numlock\fR, \fB\-\-capslock\fR and \fB\-\-scrollock\fR options, -the respective bindings can be set. -\fIslot\fR is the number of the HOTP slot to bind the key to. -If \fB\-\-no\-numlock\fR, \fB\-\-no\-capslock\fR or \fB\-\-no\-scrollock\fR is -set, the respective binding is disabled. -The two corresponding options are mutually exclusive. - -If \fB\-\-otp\-pin\fR is set, the user PIN will be required to generate one-time -passwords using the \fBotp get\fR command. -If \fB\-\-no\-otp\-pin\fR is set, OTP generation can be performed without PIN. -These two options are mutually exclusive. - -.SS Password safe -The Nitrokey Pro and the Nitrokey Storage provide a password safe (PWS) with 20 -slots. -In each of these slots you can store a name, a login, and a password. -The PWS is not encrypted, but it is protected with the user PIN by the firmware. -Once the PWS is unlocked by one of the commands listed below, it can be -accessed without authentication. -You can use the \fBlock\fR command to lock the password safe. -.TP -\fBnitrocli pws get \fIslot \fR[\fB\-n\fR|\fB\-\-name\fR] \ -[\fB\-l\fR|\fB\-\-login\fR] \ -[\fB\-p\fR|\fB\-\-password\fR] \ -[\fB\-q\fR|\fB\-\-quiet\fR] -Print the content of one PWS slot. -\fIslot\fR is the number of the slot. -Per default, this command prints the name, the login and the password (in that -order). -If one or more of the options \fB\-\-name\fR, \fB\-\-login\fR, and -\fB\-\-password\fR are set, only the selected fields are printed. -The order of the fields never changes. - -The fields are printed together with a label. -Use the \fB\-\-quiet\fR option to suppress the labels and to only output the -values stored in the PWS slot. -.TP -\fBnitrocli pws set \fIslot name login password\fR -Set the content of a PWS slot. -\fIslot\fR is the number of the slot to write. -\fIname\fR, \fIlogin\fR, and \fIpassword\fR represent the data to write to the -slot. -.TP -\fBnitrocli pws clear \fIslot\fR -Delete the data stored in a PWS slot. -\fIslot\fR is the number of the slot clear. -.TP -\fBnitrocli pws status \fR[\fB\-a\fR|\fB\-\-all\fR] -List all PWS slots. -If \fB\-\-all\fR is not set, empty slots are ignored. - -.SS PINs -Nitrokey devices have two PINs: the user PIN and the admin PIN. The user -PIN must have at least six, the admin PIN at least eight characters. The -user PIN is required for commands such as \fBotp get\fR (depending on -the configuration) and for all \fBpws\fR commands. -The admin PIN is usually required to change the device configuration. -.P -Each PIN has a retry counter that is decreased with every wrong PIN entry and -reset if the PIN was entered correctly. -The initial retry counter is three. -If the retry counter for the user PIN is zero, you can use the -\fBpin unblock\fR command to unblock and reset the user PIN. -If the retry counter for the admin PIN is zero, you have to perform a factory -reset using the \fBreset\fR command or \fBgpg\fR(1). -Use the \fBstatus\fR command to check the retry counters. -.TP -.B nitrocli pin clear -Clear the PINs cached by the other commands. Note that cached PINs are -associated with the device they belong to and the \fBclear\fR command will only -clear the PIN for the currently used device, not all others. -.TP -\fBnitrocli pin set \fItype\fR -Change a PIN. -\fItype\fR is the type of the PIN that will be changed: \fBadmin\fR to change -the admin PIN or \fBuser\fR to change the user PIN. -This command only works if the retry counter for the PIN type is at least one. -(Use the \fBstatus\fR command to check the retry counters.) -.TP -.B nitrocli pin unblock -Unblock and reset the user PIN. -This command requires the admin PIN. -The admin PIN cannot be unblocked. -This operation is equivalent to the unblock PIN option provided by \fBgpg\fR(1) -(using the \fB\-\-change\-pin\fR option). - -.SH ENVIRONMENT -The program honors a set of environment variables that can be used to -suppress interactive PIN entry through \fBpinentry\fR(1). The following -variables are recognized: -.TP -.B NITROCLI_ADMIN_PIN -The admin PIN to use. -.TP -.B NITROCLI_USER_PIN -The user PIN to use. -.TP -.B NITROCLI_NEW_ADMIN_PIN -The new admin PIN to set. This variable is only used by the \fBpin set\fR -command for the \fBadmin\fR type. -.TP -.B NITROCLI_NEW_USER_PIN -The new user PIN to set. This variable is only used by the \fBpin set\fR command -for the \fBuser\fR type. -.TP -.B NITROCLI_PASSWORD -A password used by commands that require one (e.g., \fBhidden open\fR). -.TP -.B NITROCLI_NO_CACHE -If this variable is present in the environment, do not cache any inquired -secrets using \fBgpg\-agent\fR(1) but ask for them each time they are needed. -Note that this variable does not cause any cached secrets to be cleared. If a -secret is already in the cache it will be ignored, but left otherwise untouched. -Use the \fBpin clear\fR command to clear secrets from the cache. - -.SH EXAMPLES -.SS Storage -Create a hidden volume in the first available slot, starting at half the size of -the encrypted volume (i.e., 50%) and stretching all the way to its end (100%): - $ \fBnitrocli hidden create 0 50 100\fR - -.SS One-time passwords -Configure a one-time password slot with a hexadecimal secret representation: - $ \fBnitrocli otp set 0 test\-rfc4226 3132333435363738393031323334353637383930 \-\-algorithm hotp\fR - $ \fBnitrocli otp set 1 test\-foobar 666F6F626172 \-\-algorithm hotp\fR - $ \fBnitrocli otp set 0 test\-rfc6238 3132333435363738393031323334353637383930 \-\-algorithm totp \-\-digits 8\fR -.P -Configure a one-time password slot with an ASCII secret representation: - $ \fBnitrocli otp set 0 test\-rfc4226 12345678901234567890 \-\-format ascii \-\-algorithm hotp\fR - $ \fBnitrocli otp set 1 test\-foobar foobar \-\-format ascii \-\-algorithm hotp\fR - $ \fBnitrocli otp set 0 test\-rfc6238 12345678901234567890 \-\-format ascii \-\-algorithm totp \-\-digits 8\fR -.P -Configure a one-time password slot with a base32 secret representation: - $ \fBnitrocli otp set 0 test\-rfc4226 gezdgnbvgy3tqojqgezdgnbvgy3tqojq \-\-format base32 \-\-algorithm hotp\fR - $ \fBnitrocli otp set 1 test\-foobar mzxw6ytboi====== \-\-format base32 \-\-algorithm hotp\fR - $ \fBnitrocli otp set 0 test\-rfc6238 gezdgnbvgy3tqojqgezdgnbvgy3tqojq \-\-format base32 \-\-algorithm totp \-\-digits 8\fR -.P -Generate a one-time password: - $ \fBnitrocli otp get 0 \-\-algorithm hotp\fR - 755224 - $ \fBnitrocli otp get 0 \-\-algorithm totp \-\-time 1234567890\fR - 89005924 -.P -Clear a one-time password slot: - $ \fBnitrocli otp clear 0 \-\-algorithm hotp\fR - -.SS Configuration -Query the configuration: - $ \fBnitrocli config get\fR - Config: - numlock binding: not set - capslock binding: not set - scrollock binding: not set - require user PIN for OTP: true -.P -Change the configuration: - $ \fBnitrocli config set \-\-otp\-pin\fR - -.SS Password safe -Configure a PWS slot: - $ \fBnitrocli pws set 0 example.org john.doe passw0rd\fR - -Get the data from a slot: - $ \fBnitrocli pws get 0\fR - name: example.org - login: john.doe - password: passw0rd - -Copy the password to the clipboard (requires \fBxclip\fR(1)). - $ \fBnitrocli pws get 0 \-\-password \-\-quiet | xclip \-in\fR - -Query the PWS slots: - $ \fB nitrocli pws status\fR - slot name - 0 example.org diff --git a/nitrocli/doc/nitrocli.1.pdf b/nitrocli/doc/nitrocli.1.pdf deleted file mode 100644 index cd15a66..0000000 Binary files a/nitrocli/doc/nitrocli.1.pdf and /dev/null differ diff --git a/nitrocli/doc/packaging.md b/nitrocli/doc/packaging.md deleted file mode 100644 index 5ff4089..0000000 --- a/nitrocli/doc/packaging.md +++ /dev/null @@ -1,64 +0,0 @@ -How to package nitrocli -======================= - -This document describes how to update the packaged versions of nitrocli. - -Arch Linux ----------- - -1. Clone the Git repository at ssh://aur@aur.archlinux.org/nitrocli.git. -2. Edit the `PKGBUILD` file: - - Update the `pkgver` variable to the current nitrocli version. - - If the `pkgrel` variable is not 1, set it to 1. - - Update the SHA512 hash in the `sha512sums` variable for the new tarball. -3. Update the `.SRCINFO` file by running `makepkg --printsrcinfo > .SRCINFO`. -4. Verify that the package builds successfully by running `makepkg`. -5. Verify that the package was built as expected by running `pacman -Qlp $f` - and `pacman -Qip $f`, where `$f` is `nitrocli-$pkgver.pkg.tar.gz`. -6. Check the package for errors by running `namcap PKGBUILD` and `namcap - nitrocli-$pkgver.pkg.tar.gz`. -7. Add, commit, and push your changes to publish them in the AUR. - -If you have to release a new package version without a new nitrocli version, -do not change the `pkgver` variable and instead increment the `pkgrel` -variable. - -For more information, see the [Arch User Repository][] page in the Arch Wiki. - -Debian ------- - -1. Clone or fork the Git repository at - https://salsa.debian.org/rust-team/debcargo-conf. -2. Execute `./update.sh nitrocli`. -3. Check and, if necessary, update the Debian changelog in the file - `src/nitrocli/debian/changelog`. -4. Verify that the package builds successfully by running `./build.sh nitrocli` - in the `build` directory. (This requires an `sbuild` environment as - described in the `README.rst` file.) -5. Inspect the generated package by running `dpkg-deb --info` and `dpkg-deb - --contents` on it. -6. If you have push access to the repository, create the - `src/nitrocli/debian/RFS` file to indicate that `nitrocli` can be updated. -7. Add and commit your changes. If you have push access, push them. - Otherwise create a merge request and indicate that `nitrocli` is ready for - upload in its description. - -For more information, see the [Teams/RustPackaging][] page in the Debian Wiki -and the [README.rst file][] in the debcargo-conf repository. - -For detailed information on the status of the Debian package, check the [Debian -Package Tracker][]. - -Ubuntu ------- - -The `nitrocli` package for Ubuntu is automatically generated from the Debian -package. For detailed information on the status of the Ubuntu package, check -[Launchpad][]. - -[Arch User Repository]: https://wiki.archlinux.org/index.php/Arch_User_Repository -[Teams/RustPackaging]: https://wiki.debian.org/Teams/RustPackaging -[README.rst file]: https://salsa.debian.org/rust-team/debcargo-conf/blob/master/README.rst -[Debian Package Tracker]: https://tracker.debian.org/pkg/rust-nitrocli -[Launchpad]: https://launchpad.net/ubuntu/+source/rust-nitrocli diff --git a/nitrocli/rustfmt.toml b/nitrocli/rustfmt.toml deleted file mode 100644 index b196eaa..0000000 --- a/nitrocli/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -tab_spaces = 2 diff --git a/nitrocli/src/arg_util.rs b/nitrocli/src/arg_util.rs deleted file mode 100644 index e2e7b1d..0000000 --- a/nitrocli/src/arg_util.rs +++ /dev/null @@ -1,158 +0,0 @@ -// arg_util.rs - -// ************************************************************************* -// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -macro_rules! count { - ($head:ident) => { 1 }; - ($head:ident, $($tail:ident),*) => { - 1 + count!($($tail),*) - } -} - -/// A macro for generating an enum with a set of simple (i.e., no -/// parameters) variants and their textual representations. -// TODO: Right now we hard code the derives we create. We may want to -// make this set configurable. -macro_rules! Enum { - ( $name:ident, [ $( $var:ident => ($str:expr, $exec:expr), ) *] ) => { - Enum! {$name, [ - $( $var => $str, )* - ]} - - #[allow(unused_qualifications)] - impl $name { - fn execute( - self, - ctx: &mut crate::args::ExecCtx<'_>, - args: ::std::vec::Vec<::std::string::String>, - ) -> crate::Result<()> { - match self { - $( - $name::$var => $exec(ctx, args), - )* - } - } - } - }; - ( $name:ident, [ $( $var:ident => $str:expr, ) *] ) => { - #[derive(Clone, Copy, Debug, PartialEq)] - pub enum $name { - $( - $var, - )* - } - - impl $name { - #[allow(unused)] - pub fn all(&self) -> [$name; count!($($var),*) ] { - $name::all_variants() - } - - pub fn all_variants() -> [$name; count!($($var),*) ] { - [ - $( - $name::$var, - )* - ] - } - } - - impl ::std::convert::AsRef for $name { - fn as_ref(&self) -> &'static str { - match *self { - $( - $name::$var => $str, - )* - } - } - } - - impl ::std::fmt::Display for $name { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - write!(f, "{}", self.as_ref()) - } - } - - impl ::std::str::FromStr for $name { - type Err = (); - - fn from_str(s: &str) -> ::std::result::Result { - match s { - $( - $str => Ok($name::$var), - )* - _ => Err(()), - } - } - } - }; -} - -/// A macro for formatting the variants of an enum (as created by the -/// Enum!{} macro) ready to be used in a help text. The supplied `fmt` -/// needs to contain the named parameter `{variants}`, which will be -/// replaced with a generated version of the enum's variants. -macro_rules! fmt_enum { - ( $enm:ident ) => {{ - fmt_enum!($enm.all()) - }}; - ( $all:expr ) => {{ - $all - .iter() - .map(::std::convert::AsRef::as_ref) - .collect::<::std::vec::Vec<_>>() - .join("|") - }}; -} - -/// A macro for generating the help text for a command/subcommand. The -/// argument is the variable representing the command (which in turn is -/// an enum). -/// Note that the name of this variable is embedded into the help text! -macro_rules! cmd_help { - ( $cmd:ident ) => { - format!( - concat!("The ", stringify!($cmd), " to execute ({})"), - fmt_enum!($cmd) - ) - }; -} - -#[cfg(test)] -mod tests { - Enum! {Command, [ - Var1 => "var1", - Var2 => "2", - Var3 => "crazy", - ]} - - #[test] - fn all_variants() { - assert_eq!( - Command::all_variants(), - [Command::Var1, Command::Var2, Command::Var3] - ) - } - - #[test] - fn text_representations() { - assert_eq!(Command::Var1.as_ref(), "var1"); - assert_eq!(Command::Var2.as_ref(), "2"); - assert_eq!(Command::Var3.as_ref(), "crazy"); - } -} diff --git a/nitrocli/src/args.rs b/nitrocli/src/args.rs deleted file mode 100644 index 9f4cae2..0000000 --- a/nitrocli/src/args.rs +++ /dev/null @@ -1,984 +0,0 @@ -// args.rs - -// ************************************************************************* -// * Copyright (C) 2018-2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use std::ffi; -use std::io; -use std::result; -use std::str; - -use crate::commands; -use crate::error::Error; -use crate::pinentry; -use crate::RunCtx; - -type Result = result::Result; - -/// Wraps a writer and buffers its output. -/// -/// This implementation is similar to `io::BufWriter`, but: -/// - The inner writer is only written to if `flush` is called. -/// - The buffer may grow infinitely large. -struct BufWriter<'w, W: io::Write + ?Sized> { - buf: Vec, - inner: &'w mut W, -} - -impl<'w, W: io::Write + ?Sized> BufWriter<'w, W> { - pub fn new(inner: &'w mut W) -> Self { - BufWriter { - buf: Vec::with_capacity(128), - inner, - } - } -} - -impl<'w, W: io::Write + ?Sized> io::Write for BufWriter<'w, W> { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.buf.extend_from_slice(buf); - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - self.inner.write_all(&self.buf)?; - self.buf.clear(); - self.inner.flush() - } -} - -trait Stdio { - fn stdio(&mut self) -> (&mut dyn io::Write, &mut dyn io::Write); -} - -impl<'io> Stdio for RunCtx<'io> { - fn stdio(&mut self) -> (&mut dyn io::Write, &mut dyn io::Write) { - (self.stdout, self.stderr) - } -} - -impl Stdio for (&mut W, &mut W) -where - W: io::Write, -{ - fn stdio(&mut self) -> (&mut dyn io::Write, &mut dyn io::Write) { - (self.0, self.1) - } -} - -/// A command execution context that captures additional data pertaining -/// the command execution. -pub struct ExecCtx<'io> { - pub model: Option, - pub stdout: &'io mut dyn io::Write, - pub stderr: &'io mut dyn io::Write, - pub admin_pin: Option, - pub user_pin: Option, - pub new_admin_pin: Option, - pub new_user_pin: Option, - pub password: Option, - pub no_cache: bool, - pub verbosity: u64, -} - -impl<'io> Stdio for ExecCtx<'io> { - fn stdio(&mut self) -> (&mut dyn io::Write, &mut dyn io::Write) { - (self.stdout, self.stderr) - } -} - -/// The available Nitrokey models. -#[allow(unused_doc_comments)] -Enum! {DeviceModel, [ - Pro => "pro", - Storage => "storage", -]} - -impl DeviceModel { - pub fn as_user_facing_str(&self) -> &str { - match self { - DeviceModel::Pro => "Pro", - DeviceModel::Storage => "Storage", - } - } -} - -impl From for nitrokey::Model { - fn from(model: DeviceModel) -> nitrokey::Model { - match model { - DeviceModel::Pro => nitrokey::Model::Pro, - DeviceModel::Storage => nitrokey::Model::Storage, - } - } -} - -/// A top-level command for nitrocli. -#[allow(unused_doc_comments)] -Enum! {Command, [ - Config => ("config", config), - Encrypted => ("encrypted", encrypted), - Hidden => ("hidden", hidden), - Lock => ("lock", lock), - Otp => ("otp", otp), - Pin => ("pin", pin), - Pws => ("pws", pws), - Reset => ("reset", reset), - Status => ("status", status), - Unencrypted => ("unencrypted", unencrypted), -]} - -Enum! {ConfigCommand, [ - Get => ("get", config_get), - Set => ("set", config_set), -]} - -#[derive(Clone, Copy, Debug)] -pub enum ConfigOption { - Enable(T), - Disable, - Ignore, -} - -impl ConfigOption { - fn try_from(disable: bool, value: Option, name: &'static str) -> Result { - if disable { - if value.is_some() { - Err(Error::Error(format!( - "--{name} and --no-{name} are mutually exclusive", - name = name - ))) - } else { - Ok(ConfigOption::Disable) - } - } else { - match value { - Some(value) => Ok(ConfigOption::Enable(value)), - None => Ok(ConfigOption::Ignore), - } - } - } - - pub fn or(self, default: Option) -> Option { - match self { - ConfigOption::Enable(value) => Some(value), - ConfigOption::Disable => None, - ConfigOption::Ignore => default, - } - } -} - -Enum! {OtpCommand, [ - Clear => ("clear", otp_clear), - Get => ("get", otp_get), - Set => ("set", otp_set), - Status => ("status", otp_status), -]} - -Enum! {OtpAlgorithm, [ - Hotp => "hotp", - Totp => "totp", -]} - -Enum! {OtpMode, [ - SixDigits => "6", - EightDigits => "8", -]} - -impl From for nitrokey::OtpMode { - fn from(mode: OtpMode) -> Self { - match mode { - OtpMode::SixDigits => nitrokey::OtpMode::SixDigits, - OtpMode::EightDigits => nitrokey::OtpMode::EightDigits, - } - } -} - -Enum! {OtpSecretFormat, [ - Ascii => "ascii", - Base32 => "base32", - Hex => "hex", -]} - -Enum! {PinCommand, [ - Clear => ("clear", pin_clear), - Set => ("set", pin_set), - Unblock => ("unblock", pin_unblock), -]} - -Enum! {PwsCommand, [ - Clear => ("clear", pws_clear), - Get => ("get", pws_get), - Set => ("set", pws_set), - Status => ("status", pws_status), -]} - -fn parse( - ctx: &mut impl Stdio, - parser: argparse::ArgumentParser<'_>, - args: Vec, -) -> Result<()> { - let (stdout, stderr) = ctx.stdio(); - let result = parser - .parse(args, stdout, stderr) - .map_err(Error::ArgparseError); - drop(parser); - result -} - -/// Inquire the status of the Nitrokey. -fn status(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Prints the status of the connected Nitrokey device"); - parse(ctx, parser, args)?; - - commands::status(ctx) -} - -/// Perform a factory reset. -fn reset(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Performs a factory reset"); - parse(ctx, parser, args)?; - - commands::reset(ctx) -} - -Enum! {UnencryptedCommand, [ - Set => ("set", unencrypted_set), -]} - -Enum! {UnencryptedVolumeMode, [ - ReadWrite => "read-write", - ReadOnly => "read-only", -]} - -/// Execute an unencrypted subcommand. -fn unencrypted(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut subcommand = UnencryptedCommand::Set; - let help = cmd_help!(subcommand); - let mut subargs = vec![]; - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Interacts with the device's unencrypted volume"); - let _ = - parser - .refer(&mut subcommand) - .required() - .add_argument("subcommand", argparse::Store, &help); - let _ = parser.refer(&mut subargs).add_argument( - "arguments", - argparse::List, - "The arguments for the subcommand", - ); - parser.stop_on_first_argument(true); - parse(ctx, parser, args)?; - - subargs.insert( - 0, - format!( - "{} {} {}", - crate::NITROCLI, - Command::Unencrypted, - subcommand, - ), - ); - subcommand.execute(ctx, subargs) -} - -/// Change the configuration of the unencrypted volume. -fn unencrypted_set(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut mode = UnencryptedVolumeMode::ReadWrite; - let help = format!("The mode to change to ({})", fmt_enum!(mode)); - let mut parser = argparse::ArgumentParser::new(); - parser - .set_description("Changes the configuration of the unencrypted volume on a Nitrokey Storage"); - let _ = parser - .refer(&mut mode) - .required() - .add_argument("type", argparse::Store, &help); - parse(ctx, parser, args)?; - - commands::unencrypted_set(ctx, mode) -} - -Enum! {EncryptedCommand, [ - Close => ("close", encrypted_close), - Open => ("open", encrypted_open), -]} - -/// Execute an encrypted subcommand. -fn encrypted(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut subcommand = EncryptedCommand::Open; - let help = cmd_help!(subcommand); - let mut subargs = vec![]; - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Interacts with the device's encrypted volume"); - let _ = - parser - .refer(&mut subcommand) - .required() - .add_argument("subcommand", argparse::Store, &help); - let _ = parser.refer(&mut subargs).add_argument( - "arguments", - argparse::List, - "The arguments for the subcommand", - ); - parser.stop_on_first_argument(true); - parse(ctx, parser, args)?; - - subargs.insert( - 0, - format!("{} {} {}", crate::NITROCLI, Command::Encrypted, subcommand), - ); - subcommand.execute(ctx, subargs) -} - -/// Open the encrypted volume on the Nitrokey. -fn encrypted_open(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Opens the encrypted volume on a Nitrokey Storage"); - parse(ctx, parser, args)?; - - commands::encrypted_open(ctx) -} - -/// Close the previously opened encrypted volume. -fn encrypted_close(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Closes the encrypted volume on a Nitrokey Storage"); - parse(ctx, parser, args)?; - - commands::encrypted_close(ctx) -} - -Enum! {HiddenCommand, [ - Close => ("close", hidden_close), - Create => ("create", hidden_create), - Open => ("open", hidden_open), -]} - -/// Execute a hidden subcommand. -fn hidden(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut subcommand = HiddenCommand::Open; - let help = cmd_help!(subcommand); - let mut subargs = vec![]; - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Interacts with the device's hidden volume"); - let _ = - parser - .refer(&mut subcommand) - .required() - .add_argument("subcommand", argparse::Store, &help); - let _ = parser.refer(&mut subargs).add_argument( - "arguments", - argparse::List, - "The arguments for the subcommand", - ); - parser.stop_on_first_argument(true); - parse(ctx, parser, args)?; - - subargs.insert( - 0, - format!("{} {} {}", crate::NITROCLI, Command::Hidden, subcommand), - ); - subcommand.execute(ctx, subargs) -} - -fn hidden_create(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut slot: u8 = 0; - let mut start: u8 = 0; - let mut end: u8 = 0; - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Creates a hidden volume on a Nitrokey Storage"); - let _ = parser.refer(&mut slot).required().add_argument( - "slot", - argparse::Store, - "The hidden volume slot to use", - ); - let _ = parser.refer(&mut start).required().add_argument( - "start", - argparse::Store, - "The start location of the hidden volume as percentage of the \ - encrypted volume's size (0-99)", - ); - let _ = parser.refer(&mut end).required().add_argument( - "end", - argparse::Store, - "The end location of the hidden volume as percentage of the \ - encrypted volume's size (1-100)", - ); - parse(ctx, parser, args)?; - - commands::hidden_create(ctx, slot, start, end) -} - -fn hidden_open(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Opens a hidden volume on a Nitrokey Storage"); - parse(ctx, parser, args)?; - - commands::hidden_open(ctx) -} - -fn hidden_close(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Closes the hidden volume on a Nitrokey Storage"); - parse(ctx, parser, args)?; - - commands::hidden_close(ctx) -} - -/// Execute a config subcommand. -fn config(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut subcommand = ConfigCommand::Get; - let help = cmd_help!(subcommand); - let mut subargs = vec![]; - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Reads or writes the device configuration"); - let _ = - parser - .refer(&mut subcommand) - .required() - .add_argument("subcommand", argparse::Store, &help); - let _ = parser.refer(&mut subargs).add_argument( - "arguments", - argparse::List, - "The arguments for the subcommand", - ); - parser.stop_on_first_argument(true); - parse(ctx, parser, args)?; - - subargs.insert( - 0, - format!("{} {} {}", crate::NITROCLI, Command::Config, subcommand), - ); - subcommand.execute(ctx, subargs) -} - -/// Read the Nitrokey configuration. -fn config_get(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Prints the Nitrokey configuration"); - parse(ctx, parser, args)?; - - commands::config_get(ctx) -} - -/// Write the Nitrokey configuration. -fn config_set(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut numlock = None; - let mut no_numlock = false; - let mut capslock = None; - let mut no_capslock = false; - let mut scrollock = None; - let mut no_scrollock = false; - let mut otp_pin = false; - let mut no_otp_pin = false; - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Changes the Nitrokey configuration"); - let _ = parser.refer(&mut numlock).add_option( - &["-n", "--numlock"], - argparse::StoreOption, - "Set the numlock option to the given HOTP slot", - ); - let _ = parser.refer(&mut no_numlock).add_option( - &["-N", "--no-numlock"], - argparse::StoreTrue, - "Unset the numlock option", - ); - let _ = parser.refer(&mut capslock).add_option( - &["-c", "--capslock"], - argparse::StoreOption, - "Set the capslock option to the given HOTP slot", - ); - let _ = parser.refer(&mut no_capslock).add_option( - &["-C", "--no-capslock"], - argparse::StoreTrue, - "Unset the capslock option", - ); - let _ = parser.refer(&mut scrollock).add_option( - &["-s", "--scrollock"], - argparse::StoreOption, - "Set the scrollock option to the given HOTP slot", - ); - let _ = parser.refer(&mut no_scrollock).add_option( - &["-S", "--no-scrollock"], - argparse::StoreTrue, - "Unset the scrollock option", - ); - let _ = parser.refer(&mut otp_pin).add_option( - &["-o", "--otp-pin"], - argparse::StoreTrue, - "Require the user PIN to generate one-time passwords", - ); - let _ = parser.refer(&mut no_otp_pin).add_option( - &["-O", "--no-otp-pin"], - argparse::StoreTrue, - "Allow one-time password generation without PIN", - ); - parse(ctx, parser, args)?; - - let numlock = ConfigOption::try_from(no_numlock, numlock, "numlock")?; - let capslock = ConfigOption::try_from(no_capslock, capslock, "capslock")?; - let scrollock = ConfigOption::try_from(no_scrollock, scrollock, "scrollock")?; - let otp_pin = if otp_pin { - Some(true) - } else if no_otp_pin { - Some(false) - } else { - None - }; - commands::config_set(ctx, numlock, capslock, scrollock, otp_pin) -} - -/// Lock the Nitrokey. -fn lock(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Locks the connected Nitrokey device"); - parse(ctx, parser, args)?; - - commands::lock(ctx) -} - -/// Execute an OTP subcommand. -fn otp(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut subcommand = OtpCommand::Get; - let help = cmd_help!(subcommand); - let mut subargs = vec![]; - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Accesses one-time passwords"); - let _ = - parser - .refer(&mut subcommand) - .required() - .add_argument("subcommand", argparse::Store, &help); - let _ = parser.refer(&mut subargs).add_argument( - "arguments", - argparse::List, - "The arguments for the subcommand", - ); - parser.stop_on_first_argument(true); - parse(ctx, parser, args)?; - - subargs.insert( - 0, - format!("{} {} {}", crate::NITROCLI, Command::Otp, subcommand), - ); - subcommand.execute(ctx, subargs) -} - -/// Generate a one-time password on the Nitrokey device. -fn otp_get(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut slot: u8 = 0; - let mut algorithm = OtpAlgorithm::Totp; - let help = format!( - "The OTP algorithm to use ({}, default: {})", - fmt_enum!(algorithm), - algorithm - ); - let mut time: Option = None; - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Generates a one-time password"); - let _ = - parser - .refer(&mut slot) - .required() - .add_argument("slot", argparse::Store, "The OTP slot to use"); - let _ = parser - .refer(&mut algorithm) - .add_option(&["-a", "--algorithm"], argparse::Store, &help); - let _ = parser.refer(&mut time).add_option( - &["-t", "--time"], - argparse::StoreOption, - "The time to use for TOTP generation (Unix timestamp, default: system time)", - ); - parse(ctx, parser, args)?; - - commands::otp_get(ctx, slot, algorithm, time) -} - -/// Configure a one-time password slot on the Nitrokey device. -pub fn otp_set(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut slot: u8 = 0; - let mut algorithm = OtpAlgorithm::Totp; - let algo_help = format!( - "The OTP algorithm to use ({}, default: {})", - fmt_enum!(algorithm), - algorithm - ); - let mut name = "".to_owned(); - let mut secret = "".to_owned(); - let mut digits = OtpMode::SixDigits; - let mut counter: u64 = 0; - let mut time_window: u16 = 30; - let mut secret_format = OtpSecretFormat::Hex; - let fmt_help = format!( - "The format of the secret ({}, default: {})", - fmt_enum!(OtpSecretFormat::all_variants()), - secret_format, - ); - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Configures a one-time password slot"); - let _ = - parser - .refer(&mut slot) - .required() - .add_argument("slot", argparse::Store, "The OTP slot to use"); - let _ = - parser - .refer(&mut algorithm) - .add_option(&["-a", "--algorithm"], argparse::Store, &algo_help); - let _ = parser.refer(&mut name).required().add_argument( - "name", - argparse::Store, - "The name of the slot", - ); - let _ = parser.refer(&mut secret).required().add_argument( - "secret", - argparse::Store, - "The secret to store on the slot as a hexadecimal string (unless overwritten by --format)", - ); - let _ = parser.refer(&mut digits).add_option( - &["-d", "--digits"], - argparse::Store, - "The number of digits to use for the one-time password (6 or 8, default: 6)", - ); - let _ = parser.refer(&mut counter).add_option( - &["-c", "--counter"], - argparse::Store, - "The counter value for HOTP (default: 0)", - ); - let _ = parser.refer(&mut time_window).add_option( - &["-t", "--time-window"], - argparse::Store, - "The time window for TOTP (default: 30)", - ); - let _ = - parser - .refer(&mut secret_format) - .add_option(&["-f", "--format"], argparse::Store, &fmt_help); - parse(ctx, parser, args)?; - - let data = nitrokey::OtpSlotData { - number: slot, - name, - secret, - mode: nitrokey::OtpMode::from(digits), - use_enter: false, - token_id: None, - }; - commands::otp_set(ctx, data, algorithm, counter, time_window, secret_format) -} - -/// Clear an OTP slot. -fn otp_clear(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut slot: u8 = 0; - let mut algorithm = OtpAlgorithm::Totp; - let help = format!( - "The OTP algorithm to use ({}, default: {})", - fmt_enum!(algorithm), - algorithm - ); - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Clears a one-time password slot"); - let _ = parser.refer(&mut slot).required().add_argument( - "slot", - argparse::Store, - "The OTP slot to clear", - ); - let _ = parser - .refer(&mut algorithm) - .add_option(&["-a", "--algorithm"], argparse::Store, &help); - parse(ctx, parser, args)?; - - commands::otp_clear(ctx, slot, algorithm) -} - -/// Print the status of the OTP slots. -fn otp_status(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut all = false; - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Prints the status of the OTP slots"); - let _ = parser.refer(&mut all).add_option( - &["-a", "--all"], - argparse::StoreTrue, - "Show slots that are not programmed", - ); - parse(ctx, parser, args)?; - - commands::otp_status(ctx, all) -} - -/// Execute a PIN subcommand. -fn pin(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut subcommand = PinCommand::Clear; - let help = cmd_help!(subcommand); - let mut subargs = vec![]; - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Manages the Nitrokey PINs"); - let _ = - parser - .refer(&mut subcommand) - .required() - .add_argument("subcommand", argparse::Store, &help); - let _ = parser.refer(&mut subargs).add_argument( - "arguments", - argparse::List, - "The arguments for the subcommand", - ); - parser.stop_on_first_argument(true); - parse(ctx, parser, args)?; - - subargs.insert( - 0, - format!("{} {} {}", crate::NITROCLI, Command::Pin, subcommand), - ); - subcommand.execute(ctx, subargs) -} - -/// Clear the PIN as cached by various other commands. -fn pin_clear(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Clears the cached PINs"); - parse(ctx, parser, args)?; - - commands::pin_clear(ctx) -} - -/// Change a PIN. -fn pin_set(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut pintype = pinentry::PinType::User; - let help = format!("The PIN type to change ({})", fmt_enum!(pintype)); - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Changes a PIN"); - let _ = parser - .refer(&mut pintype) - .required() - .add_argument("type", argparse::Store, &help); - parse(ctx, parser, args)?; - - commands::pin_set(ctx, pintype) -} - -/// Unblock and reset the user PIN. -fn pin_unblock(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Unblocks and resets the user PIN"); - parse(ctx, parser, args)?; - - commands::pin_unblock(ctx) -} - -/// Execute a PWS subcommand. -fn pws(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut subcommand = PwsCommand::Get; - let mut subargs = vec![]; - let help = cmd_help!(subcommand); - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Accesses the password safe"); - let _ = - parser - .refer(&mut subcommand) - .required() - .add_argument("subcommand", argparse::Store, &help); - let _ = parser.refer(&mut subargs).add_argument( - "arguments", - argparse::List, - "The arguments for the subcommand", - ); - parser.stop_on_first_argument(true); - parse(ctx, parser, args)?; - - subargs.insert( - 0, - format!("{} {} {}", crate::NITROCLI, Command::Pws, subcommand), - ); - subcommand.execute(ctx, subargs) -} - -/// Access a slot of the password safe on the Nitrokey. -fn pws_get(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut slot: u8 = 0; - let mut name = false; - let mut login = false; - let mut password = false; - let mut quiet = false; - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Reads a password safe slot"); - let _ = parser.refer(&mut slot).required().add_argument( - "slot", - argparse::Store, - "The PWS slot to read", - ); - let _ = parser.refer(&mut name).add_option( - &["-n", "--name"], - argparse::StoreTrue, - "Show the name stored on the slot", - ); - let _ = parser.refer(&mut login).add_option( - &["-l", "--login"], - argparse::StoreTrue, - "Show the login stored on the slot", - ); - let _ = parser.refer(&mut password).add_option( - &["-p", "--password"], - argparse::StoreTrue, - "Show the password stored on the slot", - ); - let _ = parser.refer(&mut quiet).add_option( - &["-q", "--quiet"], - argparse::StoreTrue, - "Print the stored data without description", - ); - parse(ctx, parser, args)?; - - commands::pws_get(ctx, slot, name, login, password, quiet) -} - -/// Set a slot of the password safe on the Nitrokey. -fn pws_set(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut slot: u8 = 0; - let mut name = String::new(); - let mut login = String::new(); - let mut password = String::new(); - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Writes a password safe slot"); - let _ = parser.refer(&mut slot).required().add_argument( - "slot", - argparse::Store, - "The PWS slot to write", - ); - let _ = parser.refer(&mut name).required().add_argument( - "name", - argparse::Store, - "The name to store on the slot", - ); - let _ = parser.refer(&mut login).required().add_argument( - "login", - argparse::Store, - "The login to store on the slot", - ); - let _ = parser.refer(&mut password).required().add_argument( - "password", - argparse::Store, - "The password to store on the slot", - ); - parse(ctx, parser, args)?; - - commands::pws_set(ctx, slot, &name, &login, &password) -} - -/// Clear a PWS slot. -fn pws_clear(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut slot: u8 = 0; - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Clears a password safe slot"); - let _ = parser.refer(&mut slot).required().add_argument( - "slot", - argparse::Store, - "The PWS slot to clear", - ); - parse(ctx, parser, args)?; - - commands::pws_clear(ctx, slot) -} - -/// Print the status of the PWS slots. -fn pws_status(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { - let mut all = false; - let mut parser = argparse::ArgumentParser::new(); - parser.set_description("Prints the status of the PWS slots"); - let _ = parser.refer(&mut all).add_option( - &["-a", "--all"], - argparse::StoreTrue, - "Show slots that are not programmed", - ); - parse(ctx, parser, args)?; - - commands::pws_status(ctx, all) -} - -/// Parse the command-line arguments and execute the selected command. -pub(crate) fn handle_arguments(ctx: &mut RunCtx<'_>, args: Vec) -> Result<()> { - use std::io::Write; - - let mut version = false; - let mut model: Option = None; - let model_help = format!( - "Select the device model to connect to ({})", - fmt_enum!(DeviceModel::all_variants()) - ); - let mut verbosity = 0; - let mut command = Command::Status; - let cmd_help = cmd_help!(command); - let mut subargs = vec![]; - let mut parser = argparse::ArgumentParser::new(); - let _ = parser.refer(&mut version).add_option( - &["-V", "--version"], - argparse::StoreTrue, - "Print version information and exit", - ); - let _ = parser.refer(&mut verbosity).add_option( - &["-v", "--verbose"], - argparse::IncrBy::(1), - "Increase the log level (can be supplied multiple times)", - ); - let _ = - parser - .refer(&mut model) - .add_option(&["-m", "--model"], argparse::StoreOption, &model_help); - parser.set_description("Provides access to a Nitrokey device"); - let _ = parser - .refer(&mut command) - .required() - .add_argument("command", argparse::Store, &cmd_help); - let _ = parser.refer(&mut subargs).add_argument( - "arguments", - argparse::List, - "The arguments for the command", - ); - parser.stop_on_first_argument(true); - - let mut stdout_buf = BufWriter::new(ctx.stdout); - let mut stderr_buf = BufWriter::new(ctx.stderr); - let mut stdio_buf = (&mut stdout_buf, &mut stderr_buf); - let result = parse(&mut stdio_buf, parser, args); - - if version { - println!(ctx, "{} {}", crate::NITROCLI, env!("CARGO_PKG_VERSION"))?; - Ok(()) - } else { - stdout_buf.flush()?; - stderr_buf.flush()?; - - result?; - subargs.insert(0, format!("{} {}", crate::NITROCLI, command)); - - let mut ctx = ExecCtx { - model, - stdout: ctx.stdout, - stderr: ctx.stderr, - admin_pin: ctx.admin_pin.take(), - user_pin: ctx.user_pin.take(), - new_admin_pin: ctx.new_admin_pin.take(), - new_user_pin: ctx.new_user_pin.take(), - password: ctx.password.take(), - no_cache: ctx.no_cache, - verbosity, - }; - command.execute(&mut ctx, subargs) - } -} diff --git a/nitrocli/src/commands.rs b/nitrocli/src/commands.rs deleted file mode 100644 index 537a2cf..0000000 --- a/nitrocli/src/commands.rs +++ /dev/null @@ -1,984 +0,0 @@ -// commands.rs - -// ************************************************************************* -// * Copyright (C) 2018-2020 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use std::fmt; -use std::result; -use std::thread; -use std::time; -use std::u8; - -use libc::sync; - -use nitrokey::ConfigureOtp; -use nitrokey::Device; -use nitrokey::GenerateOtp; -use nitrokey::GetPasswordSafe; - -use crate::args; -use crate::error; -use crate::error::Error; -use crate::pinentry; -use crate::Result; - -/// Create an `error::Error` with an error message of the format `msg: err`. -fn get_error(msg: &'static str, err: nitrokey::Error) -> Error { - Error::NitrokeyError(Some(msg), err) -} - -/// Set `libnitrokey`'s log level based on the execution context's verbosity. -fn set_log_level(ctx: &mut args::ExecCtx<'_>) { - let log_lvl = match ctx.verbosity { - // The error log level is what libnitrokey uses by default. As such, - // there is no harm in us setting that as well when the user did not - // ask for higher verbosity. - 0 => nitrokey::LogLevel::Error, - 1 => nitrokey::LogLevel::Warning, - 2 => nitrokey::LogLevel::Info, - 3 => nitrokey::LogLevel::DebugL1, - 4 => nitrokey::LogLevel::Debug, - _ => nitrokey::LogLevel::DebugL2, - }; - nitrokey::set_log_level(log_lvl); -} - -/// Connect to any Nitrokey device and do something with it. -fn with_device(ctx: &mut args::ExecCtx<'_>, op: F) -> Result<()> -where - F: FnOnce(&mut args::ExecCtx<'_>, nitrokey::DeviceWrapper<'_>) -> Result<()>, -{ - let mut manager = nitrokey::take()?; - set_log_level(ctx); - - let device = match ctx.model { - Some(model) => manager.connect_model(model.into()).map_err(|_| { - let error = format!("Nitrokey {} device not found", model.as_user_facing_str()); - Error::Error(error) - })?, - None => manager - .connect() - .map_err(|_| Error::from("Nitrokey device not found"))?, - }; - - op(ctx, device) -} - -/// Connect to a Nitrokey Storage device and do something with it. -fn with_storage_device(ctx: &mut args::ExecCtx<'_>, op: F) -> Result<()> -where - F: FnOnce(&mut args::ExecCtx<'_>, nitrokey::Storage<'_>) -> Result<()>, -{ - let mut manager = nitrokey::take()?; - set_log_level(ctx); - - if let Some(model) = ctx.model { - if model != args::DeviceModel::Storage { - return Err(Error::from( - "This command is only available on the Nitrokey Storage", - )); - } - } - - let device = manager - .connect_storage() - .map_err(|_| Error::from("Nitrokey Storage device not found"))?; - op(ctx, device) -} - -/// Connect to any Nitrokey device, retrieve a password safe handle, and -/// do something with it. -fn with_password_safe(ctx: &mut args::ExecCtx<'_>, mut op: F) -> Result<()> -where - F: FnMut(&mut args::ExecCtx<'_>, nitrokey::PasswordSafe<'_, '_>) -> Result<()>, -{ - with_device(ctx, |ctx, mut device| { - let pin_entry = pinentry::PinEntry::from(pinentry::PinType::User, &device)?; - try_with_pin_and_data( - ctx, - &pin_entry, - "Could not access the password safe", - (), - move |ctx, _, pin| { - let pws = device - .get_password_safe(pin) - .map_err(|err| ((), Error::from(err)))?; - - op(ctx, pws).map_err(|err| ((), err)) - }, - ) - })?; - Ok(()) -} - -/// Authenticate the given device using the given PIN type and operation. -/// -/// If an error occurs, the error message `msg` is used. -fn authenticate<'mgr, D, A, F>( - ctx: &mut args::ExecCtx<'_>, - device: D, - pin_type: pinentry::PinType, - msg: &'static str, - op: F, -) -> Result -where - D: Device<'mgr>, - F: FnMut(&mut args::ExecCtx<'_>, D, &str) -> result::Result, -{ - let pin_entry = pinentry::PinEntry::from(pin_type, &device)?; - - try_with_pin_and_data(ctx, &pin_entry, msg, device, op) -} - -/// Authenticate the given device with the user PIN. -fn authenticate_user<'mgr, T>( - ctx: &mut args::ExecCtx<'_>, - device: T, -) -> Result> -where - T: Device<'mgr>, -{ - authenticate( - ctx, - device, - pinentry::PinType::User, - "Could not authenticate as user", - |_ctx, device, pin| device.authenticate_user(pin), - ) -} - -/// Authenticate the given device with the admin PIN. -fn authenticate_admin<'mgr, T>( - ctx: &mut args::ExecCtx<'_>, - device: T, -) -> Result> -where - T: Device<'mgr>, -{ - authenticate( - ctx, - device, - pinentry::PinType::Admin, - "Could not authenticate as admin", - |_ctx, device, pin| device.authenticate_admin(pin), - ) -} - -/// Return a string representation of the given volume status. -fn get_volume_status(status: &nitrokey::VolumeStatus) -> &'static str { - if status.active { - if status.read_only { - "read-only" - } else { - "active" - } - } else { - "inactive" - } -} - -/// Try to execute the given function with a pin queried using pinentry. -/// -/// This function will query the pin of the given type from the user -/// using pinentry. It will then execute the given function. If this -/// function returns a result, the result will be passed on. If it -/// returns a `CommandError::WrongPassword`, the user will be asked -/// again to enter the pin. Otherwise, this function returns an error -/// containing the given error message. The user will have at most -/// three tries to get the pin right. -/// -/// The data argument can be used to pass on data between the tries. At -/// the first try, this function will call `op` with `data`. At the -/// second or third try, it will call `op` with the data returned by the -/// previous call to `op`. -fn try_with_pin_and_data_with_pinentry( - ctx: &mut args::ExecCtx<'_>, - pin_entry: &pinentry::PinEntry, - msg: &'static str, - data: D, - mut op: F, -) -> Result -where - F: FnMut(&mut args::ExecCtx<'_>, D, &str) -> result::Result, - E: error::TryInto, -{ - let mut data = data; - let mut retry = 3; - let mut error_msg = None; - loop { - let pin = pinentry::inquire(ctx, pin_entry, pinentry::Mode::Query, error_msg)?; - match op(ctx, data, &pin) { - Ok(result) => return Ok(result), - Err((new_data, err)) => match err.try_into() { - Ok(err) => match err { - nitrokey::Error::CommandError(nitrokey::CommandError::WrongPassword) => { - pinentry::clear(pin_entry)?; - retry -= 1; - - if retry > 0 { - error_msg = Some("Wrong password, please reenter"); - data = new_data; - continue; - } - return Err(get_error(msg, err)); - } - err => return Err(get_error(msg, err)), - }, - Err(err) => return Err(err), - }, - }; - } -} - -/// Try to execute the given function with a PIN. -fn try_with_pin_and_data( - ctx: &mut args::ExecCtx<'_>, - pin_entry: &pinentry::PinEntry, - msg: &'static str, - data: D, - mut op: F, -) -> Result -where - F: FnMut(&mut args::ExecCtx<'_>, D, &str) -> result::Result, - E: Into + error::TryInto, -{ - let pin = match pin_entry.pin_type() { - // Ideally we would not clone here, but that would require us to - // restrict op to work with an immutable ExecCtx, which is not - // possible given that some clients print data. - pinentry::PinType::Admin => ctx.admin_pin.clone(), - pinentry::PinType::User => ctx.user_pin.clone(), - }; - - if let Some(pin) = pin { - let pin = pin.to_str().ok_or_else(|| { - Error::Error(format!( - "{}: Failed to read PIN due to invalid Unicode data", - msg - )) - })?; - op(ctx, data, &pin).map_err(|(_, err)| err.into()) - } else { - try_with_pin_and_data_with_pinentry(ctx, pin_entry, msg, data, op) - } -} - -/// Try to execute the given function with a pin queried using pinentry. -/// -/// This function behaves exactly as `try_with_pin_and_data`, but -/// it refrains from passing any data to it. -fn try_with_pin( - ctx: &mut args::ExecCtx<'_>, - pin_entry: &pinentry::PinEntry, - msg: &'static str, - mut op: F, -) -> Result<()> -where - F: FnMut(&str) -> result::Result<(), E>, - E: Into + error::TryInto, -{ - try_with_pin_and_data(ctx, pin_entry, msg, (), |_ctx, data, pin| { - op(pin).map_err(|err| (data, err)) - }) -} - -/// Pretty print the status of a Nitrokey Storage. -fn print_storage_status( - ctx: &mut args::ExecCtx<'_>, - status: &nitrokey::StorageStatus, -) -> Result<()> { - println!( - ctx, - r#" Storage: - SD card ID: {id:#x} - firmware: {fw} - storage keys: {sk} - volumes: - unencrypted: {vu} - encrypted: {ve} - hidden: {vh}"#, - id = status.serial_number_sd_card, - fw = if status.firmware_locked { - "locked" - } else { - "unlocked" - }, - sk = if status.stick_initialized { - "created" - } else { - "not created" - }, - vu = get_volume_status(&status.unencrypted_volume), - ve = get_volume_status(&status.encrypted_volume), - vh = get_volume_status(&status.hidden_volume), - )?; - Ok(()) -} - -/// Query and pretty print the status that is common to all Nitrokey devices. -fn print_status( - ctx: &mut args::ExecCtx<'_>, - model: &'static str, - device: &nitrokey::DeviceWrapper<'_>, -) -> Result<()> { - let serial_number = device - .get_serial_number() - .map_err(|err| get_error("Could not query the serial number", err))?; - - println!( - ctx, - r#"Status: - model: {model} - serial number: 0x{id} - firmware version: {fwv} - user retry count: {urc} - admin retry count: {arc}"#, - model = model, - id = serial_number, - fwv = device.get_firmware_version()?, - urc = device.get_user_retry_count()?, - arc = device.get_admin_retry_count()?, - )?; - - if let nitrokey::DeviceWrapper::Storage(device) = device { - let status = device - .get_status() - .map_err(|err| get_error("Getting Storage status failed", err))?; - - print_storage_status(ctx, &status) - } else { - Ok(()) - } -} - -/// Inquire the status of the nitrokey. -pub fn status(ctx: &mut args::ExecCtx<'_>) -> Result<()> { - with_device(ctx, |ctx, device| { - let model = match device { - nitrokey::DeviceWrapper::Pro(_) => "Pro", - nitrokey::DeviceWrapper::Storage(_) => "Storage", - }; - print_status(ctx, model, &device) - }) -} - -/// Perform a factory reset. -pub fn reset(ctx: &mut args::ExecCtx<'_>) -> Result<()> { - with_device(ctx, |ctx, mut device| { - let pin_entry = pinentry::PinEntry::from(pinentry::PinType::Admin, &device)?; - - // To force the user to enter the admin PIN before performing a - // factory reset, we clear the pinentry cache for the admin PIN. - pinentry::clear(&pin_entry)?; - - try_with_pin(ctx, &pin_entry, "Factory reset failed", |pin| { - device.factory_reset(&pin)?; - // Work around for a timing issue between factory_reset and - // build_aes_key, see - // https://github.com/Nitrokey/nitrokey-storage-firmware/issues/80 - thread::sleep(time::Duration::from_secs(3)); - // Another work around for spurious WrongPassword returns of - // build_aes_key after a factory reset on Pro devices. - // https://github.com/Nitrokey/nitrokey-pro-firmware/issues/57 - let _ = device.get_user_retry_count(); - device.build_aes_key(nitrokey::DEFAULT_ADMIN_PIN) - }) - }) -} - -/// Change the configuration of the unencrypted volume. -pub fn unencrypted_set( - ctx: &mut args::ExecCtx<'_>, - mode: args::UnencryptedVolumeMode, -) -> Result<()> { - with_storage_device(ctx, |ctx, mut device| { - let pin_entry = pinentry::PinEntry::from(pinentry::PinType::Admin, &device)?; - let mode = match mode { - args::UnencryptedVolumeMode::ReadWrite => nitrokey::VolumeMode::ReadWrite, - args::UnencryptedVolumeMode::ReadOnly => nitrokey::VolumeMode::ReadOnly, - }; - - // The unencrypted volume may reconnect, so be sure to flush caches to - // disk. - unsafe { sync() }; - - try_with_pin( - ctx, - &pin_entry, - "Changing unencrypted volume mode failed", - |pin| device.set_unencrypted_volume_mode(&pin, mode), - ) - }) -} - -/// Open the encrypted volume on the Nitrokey. -pub fn encrypted_open(ctx: &mut args::ExecCtx<'_>) -> Result<()> { - with_storage_device(ctx, |ctx, mut device| { - let pin_entry = pinentry::PinEntry::from(pinentry::PinType::User, &device)?; - - // We may forcefully close a hidden volume, if active, so be sure to - // flush caches to disk. - unsafe { sync() }; - - try_with_pin(ctx, &pin_entry, "Opening encrypted volume failed", |pin| { - device.enable_encrypted_volume(&pin) - }) - }) -} - -/// Close the previously opened encrypted volume. -pub fn encrypted_close(ctx: &mut args::ExecCtx<'_>) -> Result<()> { - with_storage_device(ctx, |_ctx, mut device| { - // Flush all filesystem caches to disk. We are mostly interested in - // making sure that the encrypted volume on the Nitrokey we are - // about to close is not closed while not all data was written to - // it. - unsafe { sync() }; - - device - .disable_encrypted_volume() - .map_err(|err| get_error("Closing encrypted volume failed", err)) - }) -} - -/// Create a hidden volume. -pub fn hidden_create(ctx: &mut args::ExecCtx<'_>, slot: u8, start: u8, end: u8) -> Result<()> { - with_storage_device(ctx, |ctx, mut device| { - let pwd_entry = pinentry::PwdEntry::from(&device)?; - let pwd = if let Some(pwd) = &ctx.password { - pwd - .to_str() - .ok_or_else(|| Error::from("Failed to read password: invalid Unicode data found")) - .map(ToOwned::to_owned) - } else { - pinentry::choose(ctx, &pwd_entry) - }?; - - device - .create_hidden_volume(slot, start, end, &pwd) - .map_err(|err| get_error("Creating hidden volume failed", err)) - }) -} - -/// Open a hidden volume. -pub fn hidden_open(ctx: &mut args::ExecCtx<'_>) -> Result<()> { - with_storage_device(ctx, |ctx, mut device| { - let pwd_entry = pinentry::PwdEntry::from(&device)?; - let pwd = if let Some(pwd) = &ctx.password { - pwd - .to_str() - .ok_or_else(|| Error::from("Failed to read password: invalid Unicode data found")) - .map(ToOwned::to_owned) - } else { - pinentry::inquire(ctx, &pwd_entry, pinentry::Mode::Query, None) - }?; - - // We may forcefully close an encrypted volume, if active, so be sure - // to flush caches to disk. - unsafe { sync() }; - - device - .enable_hidden_volume(&pwd) - .map_err(|err| get_error("Opening hidden volume failed", err)) - }) -} - -/// Close a previously opened hidden volume. -pub fn hidden_close(ctx: &mut args::ExecCtx<'_>) -> Result<()> { - with_storage_device(ctx, |_ctx, mut device| { - unsafe { sync() }; - - device - .disable_hidden_volume() - .map_err(|err| get_error("Closing hidden volume failed", err)) - }) -} - -/// Return a String representation of the given Option. -fn format_option(option: Option) -> String { - match option { - Some(value) => format!("{}", value), - None => "not set".to_string(), - } -} - -/// Read the Nitrokey configuration. -pub fn config_get(ctx: &mut args::ExecCtx<'_>) -> Result<()> { - with_device(ctx, |ctx, device| { - let config = device - .get_config() - .map_err(|err| get_error("Could not get configuration", err))?; - println!( - ctx, - r#"Config: - numlock binding: {nl} - capslock binding: {cl} - scrollock binding: {sl} - require user PIN for OTP: {otp}"#, - nl = format_option(config.numlock), - cl = format_option(config.capslock), - sl = format_option(config.scrollock), - otp = config.user_password, - )?; - Ok(()) - }) -} - -/// Write the Nitrokey configuration. -pub fn config_set( - ctx: &mut args::ExecCtx<'_>, - numlock: args::ConfigOption, - capslock: args::ConfigOption, - scrollock: args::ConfigOption, - user_password: Option, -) -> Result<()> { - with_device(ctx, |ctx, device| { - let mut device = authenticate_admin(ctx, device)?; - let config = device - .get_config() - .map_err(|err| get_error("Could not get configuration", err))?; - let config = nitrokey::Config { - numlock: numlock.or(config.numlock), - capslock: capslock.or(config.capslock), - scrollock: scrollock.or(config.scrollock), - user_password: user_password.unwrap_or(config.user_password), - }; - device - .write_config(config) - .map_err(|err| get_error("Could not set configuration", err)) - }) -} - -/// Lock the Nitrokey device. -pub fn lock(ctx: &mut args::ExecCtx<'_>) -> Result<()> { - with_device(ctx, |_ctx, mut device| { - device - .lock() - .map_err(|err| get_error("Could not lock the device", err)) - }) -} - -fn get_otp(slot: u8, algorithm: args::OtpAlgorithm, device: &mut T) -> Result -where - T: GenerateOtp, -{ - match algorithm { - args::OtpAlgorithm::Hotp => device.get_hotp_code(slot), - args::OtpAlgorithm::Totp => device.get_totp_code(slot), - } - .map_err(|err| get_error("Could not generate OTP", err)) -} - -fn get_unix_timestamp() -> Result { - time::SystemTime::now() - .duration_since(time::UNIX_EPOCH) - .map_err(|_| Error::from("Current system time is before the Unix epoch")) - .map(|duration| duration.as_secs()) -} - -/// Generate a one-time password on the Nitrokey device. -pub fn otp_get( - ctx: &mut args::ExecCtx<'_>, - slot: u8, - algorithm: args::OtpAlgorithm, - time: Option, -) -> Result<()> { - with_device(ctx, |ctx, mut device| { - if algorithm == args::OtpAlgorithm::Totp { - device - .set_time( - match time { - Some(time) => time, - None => get_unix_timestamp()?, - }, - true, - ) - .map_err(|err| get_error("Could not set time", err))?; - } - let config = device - .get_config() - .map_err(|err| get_error("Could not get device configuration", err))?; - let otp = if config.user_password { - let mut user = authenticate_user(ctx, device)?; - get_otp(slot, algorithm, &mut user) - } else { - get_otp(slot, algorithm, &mut device) - }?; - println!(ctx, "{}", otp)?; - Ok(()) - }) -} - -/// Format a byte vector as a hex string. -fn format_bytes(bytes: &[u8]) -> String { - bytes - .iter() - .map(|c| format!("{:02x}", c)) - .collect::>() - .join("") -} - -/// Prepare an ASCII secret string for libnitrokey. -/// -/// libnitrokey expects secrets as hexadecimal strings. This function transforms an ASCII string -/// into a hexadecimal string or returns an error if the given string contains non-ASCII -/// characters. -fn prepare_ascii_secret(secret: &str) -> Result { - if secret.is_ascii() { - Ok(format_bytes(&secret.as_bytes())) - } else { - Err(Error::from( - "The given secret is not an ASCII string despite --format ascii being set", - )) - } -} - -/// Prepare a base32 secret string for libnitrokey. -fn prepare_base32_secret(secret: &str) -> Result { - base32::decode(base32::Alphabet::RFC4648 { padding: false }, secret) - .map(|vec| format_bytes(&vec)) - .ok_or_else(|| Error::from("Could not parse base32 secret")) -} - -/// Configure a one-time password slot on the Nitrokey device. -pub fn otp_set( - ctx: &mut args::ExecCtx<'_>, - mut data: nitrokey::OtpSlotData, - algorithm: args::OtpAlgorithm, - counter: u64, - time_window: u16, - secret_format: args::OtpSecretFormat, -) -> Result<()> { - with_device(ctx, |ctx, device| { - let secret = match secret_format { - args::OtpSecretFormat::Ascii => prepare_ascii_secret(&data.secret)?, - args::OtpSecretFormat::Base32 => prepare_base32_secret(&data.secret)?, - args::OtpSecretFormat::Hex => { - // We need to ensure to provide a string with an even number of - // characters in it, just because that's what libnitrokey - // expects. So prepend a '0' if that is not the case. - // TODO: This code can be removed once upstream issue #164 - // (https://github.com/Nitrokey/libnitrokey/issues/164) is - // addressed. - if data.secret.len() % 2 != 0 { - data.secret.insert(0, '0') - } - data.secret - } - }; - let data = nitrokey::OtpSlotData { secret, ..data }; - let mut device = authenticate_admin(ctx, device)?; - match algorithm { - args::OtpAlgorithm::Hotp => device.write_hotp_slot(data, counter), - args::OtpAlgorithm::Totp => device.write_totp_slot(data, time_window), - } - .map_err(|err| get_error("Could not write OTP slot", err))?; - Ok(()) - }) -} - -/// Clear an OTP slot. -pub fn otp_clear( - ctx: &mut args::ExecCtx<'_>, - slot: u8, - algorithm: args::OtpAlgorithm, -) -> Result<()> { - with_device(ctx, |ctx, device| { - let mut device = authenticate_admin(ctx, device)?; - match algorithm { - args::OtpAlgorithm::Hotp => device.erase_hotp_slot(slot), - args::OtpAlgorithm::Totp => device.erase_totp_slot(slot), - } - .map_err(|err| get_error("Could not clear OTP slot", err))?; - Ok(()) - }) -} - -fn print_otp_status( - ctx: &mut args::ExecCtx<'_>, - algorithm: args::OtpAlgorithm, - device: &nitrokey::DeviceWrapper<'_>, - all: bool, -) -> Result<()> { - let mut slot: u8 = 0; - loop { - let result = match algorithm { - args::OtpAlgorithm::Hotp => device.get_hotp_slot_name(slot), - args::OtpAlgorithm::Totp => device.get_totp_slot_name(slot), - }; - slot = match slot.checked_add(1) { - Some(slot) => slot, - None => { - return Err(Error::from("Integer overflow when iterating OTP slots")); - } - }; - let name = match result { - Ok(name) => name, - Err(nitrokey::Error::LibraryError(nitrokey::LibraryError::InvalidSlot)) => return Ok(()), - Err(nitrokey::Error::CommandError(nitrokey::CommandError::SlotNotProgrammed)) => { - if all { - "[not programmed]".to_string() - } else { - continue; - } - } - Err(err) => return Err(get_error("Could not check OTP slot", err)), - }; - println!(ctx, "{}\t{}\t{}", algorithm, slot - 1, name)?; - } -} - -/// Print the status of the OTP slots. -pub fn otp_status(ctx: &mut args::ExecCtx<'_>, all: bool) -> Result<()> { - with_device(ctx, |ctx, device| { - println!(ctx, "alg\tslot\tname")?; - print_otp_status(ctx, args::OtpAlgorithm::Hotp, &device, all)?; - print_otp_status(ctx, args::OtpAlgorithm::Totp, &device, all)?; - Ok(()) - }) -} - -/// Clear the PIN stored by various operations. -pub fn pin_clear(ctx: &mut args::ExecCtx<'_>) -> Result<()> { - with_device(ctx, |_ctx, device| { - pinentry::clear(&pinentry::PinEntry::from( - pinentry::PinType::Admin, - &device, - )?)?; - pinentry::clear(&pinentry::PinEntry::from(pinentry::PinType::User, &device)?)?; - Ok(()) - }) -} - -/// Choose a PIN of the given type. -/// -/// If the user has set the respective environment variable for the -/// given PIN type, it will be used. -fn choose_pin( - ctx: &mut args::ExecCtx<'_>, - pin_entry: &pinentry::PinEntry, - new: bool, -) -> Result { - let new_pin = match pin_entry.pin_type() { - pinentry::PinType::Admin => { - if new { - &ctx.new_admin_pin - } else { - &ctx.admin_pin - } - } - pinentry::PinType::User => { - if new { - &ctx.new_user_pin - } else { - &ctx.user_pin - } - } - }; - - if let Some(new_pin) = new_pin { - new_pin - .to_str() - .ok_or_else(|| Error::from("Failed to read PIN: invalid Unicode data found")) - .map(ToOwned::to_owned) - } else { - pinentry::choose(ctx, pin_entry) - } -} - -/// Change a PIN. -pub fn pin_set(ctx: &mut args::ExecCtx<'_>, pin_type: pinentry::PinType) -> Result<()> { - with_device(ctx, |ctx, mut device| { - let pin_entry = pinentry::PinEntry::from(pin_type, &device)?; - let new_pin = choose_pin(ctx, &pin_entry, true)?; - - try_with_pin( - ctx, - &pin_entry, - "Could not change the PIN", - |current_pin| match pin_type { - pinentry::PinType::Admin => device.change_admin_pin(¤t_pin, &new_pin), - pinentry::PinType::User => device.change_user_pin(¤t_pin, &new_pin), - }, - )?; - - // We just changed the PIN but confirmed the action with the old PIN, - // which may have caused it to be cached. Since it no longer applies, - // make sure to evict the corresponding entry from the cache. - pinentry::clear(&pin_entry) - }) -} - -/// Unblock and reset the user PIN. -pub fn pin_unblock(ctx: &mut args::ExecCtx<'_>) -> Result<()> { - with_device(ctx, |ctx, mut device| { - let pin_entry = pinentry::PinEntry::from(pinentry::PinType::User, &device)?; - let user_pin = choose_pin(ctx, &pin_entry, false)?; - let pin_entry = pinentry::PinEntry::from(pinentry::PinType::Admin, &device)?; - - try_with_pin( - ctx, - &pin_entry, - "Could not unblock the user PIN", - |admin_pin| device.unlock_user_pin(&admin_pin, &user_pin), - ) - }) -} - -fn print_pws_data( - ctx: &mut args::ExecCtx<'_>, - description: &'static str, - result: result::Result, - quiet: bool, -) -> Result<()> { - let value = result.map_err(|err| get_error("Could not access PWS slot", err))?; - if quiet { - println!(ctx, "{}", value)?; - } else { - println!(ctx, "{} {}", description, value)?; - } - Ok(()) -} - -fn check_slot(pws: &nitrokey::PasswordSafe<'_, '_>, slot: u8) -> Result<()> { - if slot >= nitrokey::SLOT_COUNT { - return Err(nitrokey::Error::from(nitrokey::LibraryError::InvalidSlot).into()); - } - let status = pws - .get_slot_status() - .map_err(|err| get_error("Could not read PWS slot status", err))?; - if status[slot as usize] { - Ok(()) - } else { - Err(get_error( - "Could not access PWS slot", - nitrokey::CommandError::SlotNotProgrammed.into(), - )) - } -} - -/// Read a PWS slot. -pub fn pws_get( - ctx: &mut args::ExecCtx<'_>, - slot: u8, - show_name: bool, - show_login: bool, - show_password: bool, - quiet: bool, -) -> Result<()> { - with_password_safe(ctx, |ctx, pws| { - check_slot(&pws, slot)?; - - let show_all = !show_name && !show_login && !show_password; - if show_all || show_name { - print_pws_data(ctx, "name: ", pws.get_slot_name(slot), quiet)?; - } - if show_all || show_login { - print_pws_data(ctx, "login: ", pws.get_slot_login(slot), quiet)?; - } - if show_all || show_password { - print_pws_data(ctx, "password:", pws.get_slot_password(slot), quiet)?; - } - Ok(()) - }) -} - -/// Write a PWS slot. -pub fn pws_set( - ctx: &mut args::ExecCtx<'_>, - slot: u8, - name: &str, - login: &str, - password: &str, -) -> Result<()> { - with_password_safe(ctx, |_ctx, mut pws| { - pws - .write_slot(slot, name, login, password) - .map_err(|err| get_error("Could not write PWS slot", err)) - }) -} - -/// Clear a PWS slot. -pub fn pws_clear(ctx: &mut args::ExecCtx<'_>, slot: u8) -> Result<()> { - with_password_safe(ctx, |_ctx, mut pws| { - pws - .erase_slot(slot) - .map_err(|err| get_error("Could not clear PWS slot", err)) - }) -} - -fn print_pws_slot( - ctx: &mut args::ExecCtx<'_>, - pws: &nitrokey::PasswordSafe<'_, '_>, - slot: usize, - programmed: bool, -) -> Result<()> { - if slot > u8::MAX as usize { - return Err(Error::from("Invalid PWS slot number")); - } - let slot = slot as u8; - let name = if programmed { - pws - .get_slot_name(slot) - .map_err(|err| get_error("Could not read PWS slot", err))? - } else { - "[not programmed]".to_string() - }; - println!(ctx, "{}\t{}", slot, name)?; - Ok(()) -} - -/// Print the status of all PWS slots. -pub fn pws_status(ctx: &mut args::ExecCtx<'_>, all: bool) -> Result<()> { - with_password_safe(ctx, |ctx, pws| { - let slots = pws - .get_slot_status() - .map_err(|err| get_error("Could not read PWS slot status", err))?; - println!(ctx, "slot\tname")?; - for (i, &value) in slots.iter().enumerate().filter(|(_, &value)| all || value) { - print_pws_slot(ctx, &pws, i, value)?; - } - Ok(()) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn prepare_secret_ascii() { - let result = prepare_ascii_secret("12345678901234567890"); - assert_eq!( - "3132333435363738393031323334353637383930".to_string(), - result.unwrap() - ); - } - - #[test] - fn prepare_secret_non_ascii() { - let result = prepare_ascii_secret("Österreich"); - assert!(result.is_err()); - } - - #[test] - fn hex_string() { - assert_eq!(format_bytes(&[b' ']), "20"); - assert_eq!(format_bytes(&[b' ', b' ']), "2020"); - assert_eq!(format_bytes(&[b'\n', b'\n']), "0a0a"); - } -} diff --git a/nitrocli/src/error.rs b/nitrocli/src/error.rs deleted file mode 100644 index 819bed8..0000000 --- a/nitrocli/src/error.rs +++ /dev/null @@ -1,104 +0,0 @@ -// error.rs - -// ************************************************************************* -// * Copyright (C) 2017-2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use std::fmt; -use std::io; -use std::str; -use std::string; - -/// A trait used to simplify error handling in conjunction with the -/// try_with_* functions we use for repeatedly asking the user for a -/// secret. -pub trait TryInto { - fn try_into(self) -> Result; -} - -impl TryInto for T -where - T: Into, -{ - fn try_into(self) -> Result { - Ok(self.into()) - } -} - -#[derive(Debug)] -pub enum Error { - ArgparseError(i32), - IoError(io::Error), - NitrokeyError(Option<&'static str>, nitrokey::Error), - Utf8Error(str::Utf8Error), - Error(String), -} - -impl TryInto for Error { - fn try_into(self) -> Result { - match self { - Error::NitrokeyError(_, err) => Ok(err), - err => Err(err), - } - } -} - -impl From<&str> for Error { - fn from(s: &str) -> Error { - Error::Error(s.to_string()) - } -} - -impl From for Error { - fn from(e: nitrokey::Error) -> Error { - Error::NitrokeyError(None, e) - } -} - -impl From for Error { - fn from(e: io::Error) -> Error { - Error::IoError(e) - } -} - -impl From for Error { - fn from(e: str::Utf8Error) -> Error { - Error::Utf8Error(e) - } -} - -impl From for Error { - fn from(e: string::FromUtf8Error) -> Error { - Error::Utf8Error(e.utf8_error()) - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - Error::ArgparseError(_) => write!(f, "Could not parse arguments"), - Error::NitrokeyError(ref ctx, ref e) => { - if let Some(ctx) = ctx { - write!(f, "{}: ", ctx)?; - } - write!(f, "{}", e) - } - Error::Utf8Error(_) => write!(f, "Encountered UTF-8 conversion error"), - Error::IoError(ref e) => write!(f, "IO error: {}", e), - Error::Error(ref e) => write!(f, "{}", e), - } - } -} diff --git a/nitrocli/src/main.rs b/nitrocli/src/main.rs deleted file mode 100644 index c639f14..0000000 --- a/nitrocli/src/main.rs +++ /dev/null @@ -1,167 +0,0 @@ -// main.rs - -// ************************************************************************* -// * Copyright (C) 2017-2020 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -#![warn( - bad_style, - dead_code, - future_incompatible, - illegal_floating_point_literal_pattern, - improper_ctypes, - intra_doc_link_resolution_failure, - late_bound_lifetime_arguments, - missing_copy_implementations, - missing_debug_implementations, - missing_docs, - no_mangle_generic_items, - non_shorthand_field_patterns, - nonstandard_style, - overflowing_literals, - path_statements, - patterns_in_fns_without_body, - plugin_as_library, - private_in_public, - proc_macro_derive_resolution_fallback, - renamed_and_removed_lints, - rust_2018_compatibility, - rust_2018_idioms, - safe_packed_borrows, - stable_features, - trivial_bounds, - trivial_numeric_casts, - type_alias_bounds, - tyvar_behind_raw_pointer, - unconditional_recursion, - unreachable_code, - unreachable_patterns, - unstable_features, - unstable_name_collisions, - unused, - unused_comparisons, - unused_import_braces, - unused_lifetimes, - unused_qualifications, - unused_results, - where_clauses_object_safety, - while_true -)] - -//! Nitrocli is a program providing a command line interface to certain -//! commands of Nitrokey Pro and Storage devices. - -#[macro_use] -mod redefine; -#[macro_use] -mod arg_util; - -mod args; -mod commands; -mod error; -mod pinentry; -#[cfg(test)] -mod tests; - -use std::env; -use std::ffi; -use std::io; -use std::process; -use std::result; - -use crate::error::Error; - -type Result = result::Result; - -const NITROCLI: &str = "nitrocli"; -const NITROCLI_ADMIN_PIN: &str = "NITROCLI_ADMIN_PIN"; -const NITROCLI_USER_PIN: &str = "NITROCLI_USER_PIN"; -const NITROCLI_NEW_ADMIN_PIN: &str = "NITROCLI_NEW_ADMIN_PIN"; -const NITROCLI_NEW_USER_PIN: &str = "NITROCLI_NEW_USER_PIN"; -const NITROCLI_PASSWORD: &str = "NITROCLI_PASSWORD"; -const NITROCLI_NO_CACHE: &str = "NITROCLI_NO_CACHE"; - -/// The context used when running the program. -pub(crate) struct RunCtx<'io> { - /// The `Write` object used as standard output throughout the program. - pub stdout: &'io mut dyn io::Write, - /// The `Write` object used as standard error throughout the program. - pub stderr: &'io mut dyn io::Write, - /// The admin PIN, if provided through an environment variable. - pub admin_pin: Option, - /// The user PIN, if provided through an environment variable. - pub user_pin: Option, - /// The new admin PIN to set, if provided through an environment variable. - /// - /// This variable is only used by commands that change the admin PIN. - pub new_admin_pin: Option, - /// The new user PIN, if provided through an environment variable. - /// - /// This variable is only used by commands that change the user PIN. - pub new_user_pin: Option, - /// A password used by some commands, if provided through an environment variable. - pub password: Option, - /// Whether to bypass the cache for all secrets or not. - pub no_cache: bool, -} - -fn run<'ctx, 'io: 'ctx>(ctx: &'ctx mut RunCtx<'io>, args: Vec) -> i32 { - match args::handle_arguments(ctx, args) { - Ok(()) => 0, - Err(err) => match err { - Error::ArgparseError(err) => match err { - // argparse printed the help message - 0 => 0, - // argparse printed an error message - _ => 1, - }, - _ => { - let _ = eprintln!(ctx, "{}", err); - 1 - } - }, - } -} - -fn main() { - use std::io::Write; - - let mut stdout = io::stdout(); - let mut stderr = io::stderr(); - let args = env::args().collect::>(); - let ctx = &mut RunCtx { - stdout: &mut stdout, - stderr: &mut stderr, - admin_pin: env::var_os(NITROCLI_ADMIN_PIN), - user_pin: env::var_os(NITROCLI_USER_PIN), - new_admin_pin: env::var_os(NITROCLI_NEW_ADMIN_PIN), - new_user_pin: env::var_os(NITROCLI_NEW_USER_PIN), - password: env::var_os(NITROCLI_PASSWORD), - no_cache: env::var_os(NITROCLI_NO_CACHE).is_some(), - }; - - let rc = run(ctx, args); - // We exit the process the hard way below. The problem is that because - // of this, buffered IO may not be flushed. So make sure to explicitly - // flush before exiting. Note that stderr is unbuffered, alleviating - // the need for any flushing there. - // Ideally we would just make `main` return an i32 and let Rust deal - // with all of this, but the `process::Termination` functionality is - // still unstable and we have no way to convince the caller to "just - // exit" without printing additional information. - let _ = stdout.flush(); - process::exit(rc); -} diff --git a/nitrocli/src/pinentry.rs b/nitrocli/src/pinentry.rs deleted file mode 100644 index fd47657..0000000 --- a/nitrocli/src/pinentry.rs +++ /dev/null @@ -1,404 +0,0 @@ -// pinentry.rs - -// ************************************************************************* -// * Copyright (C) 2017-2020 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use std::borrow; -use std::fmt; -use std::io; -use std::process; -use std::str; - -use crate::args; -use crate::error::Error; - -type CowStr = borrow::Cow<'static, str>; - -/// PIN type requested from pinentry. -/// -/// The available PIN types correspond to the PIN types used by the Nitrokey devices: user and -/// admin. -#[allow(unused_doc_comments)] -Enum! {PinType, [ - Admin => "admin", - User => "user", -]} - -/// A trait representing a secret to be entered by the user. -pub trait SecretEntry: fmt::Debug { - /// The cache ID to use for this secret. - fn cache_id(&self) -> Option; - /// The prompt to display when asking for the secret. - fn prompt(&self) -> CowStr; - /// The description to display when asking for the secret. - fn description(&self, mode: Mode) -> CowStr; - /// The minimum number of characters the secret needs to have. - fn min_len(&self) -> u8; -} - -#[derive(Debug)] -pub struct PinEntry { - pin_type: PinType, - model: nitrokey::Model, - serial: String, -} - -impl PinEntry { - pub fn from<'mgr, D>(pin_type: PinType, device: &D) -> crate::Result - where - D: nitrokey::Device<'mgr>, - { - let model = device.get_model(); - let serial = device.get_serial_number()?; - Ok(Self { - pin_type, - model, - serial, - }) - } - - pub fn pin_type(&self) -> PinType { - self.pin_type - } -} - -impl SecretEntry for PinEntry { - fn cache_id(&self) -> Option { - let model = self.model.to_string().to_lowercase(); - let suffix = format!("{}:{}", model, self.serial); - let cache_id = match self.pin_type { - PinType::Admin => format!("nitrocli:admin:{}", suffix), - PinType::User => format!("nitrocli:user:{}", suffix), - }; - Some(cache_id.into()) - } - - fn prompt(&self) -> CowStr { - match self.pin_type { - PinType::Admin => "Admin PIN", - PinType::User => "User PIN", - } - .into() - } - - fn description(&self, mode: Mode) -> CowStr { - format!( - "{} for\rNitrokey {} {}", - match self.pin_type { - PinType::Admin => match mode { - Mode::Choose => "Please enter a new admin PIN", - Mode::Confirm => "Please confirm the new admin PIN", - Mode::Query => "Please enter the admin PIN", - }, - PinType::User => match mode { - Mode::Choose => "Please enter a new user PIN", - Mode::Confirm => "Please confirm the new user PIN", - Mode::Query => "Please enter the user PIN", - }, - }, - self.model, - self.serial, - ) - .into() - } - - fn min_len(&self) -> u8 { - match self.pin_type { - PinType::Admin => 8, - PinType::User => 6, - } - } -} - -#[derive(Debug)] -pub struct PwdEntry { - model: nitrokey::Model, - serial: String, -} - -impl PwdEntry { - pub fn from<'mgr, D>(device: &D) -> crate::Result - where - D: nitrokey::Device<'mgr>, - { - let model = device.get_model(); - let serial = device.get_serial_number()?; - Ok(Self { model, serial }) - } -} - -impl SecretEntry for PwdEntry { - fn cache_id(&self) -> Option { - None - } - - fn prompt(&self) -> CowStr { - "Password".into() - } - - fn description(&self, mode: Mode) -> CowStr { - format!( - "{} for\rNitrokey {} {}", - match mode { - Mode::Choose => "Please enter a new hidden volume password", - Mode::Confirm => "Please confirm the new hidden volume password", - Mode::Query => "Please enter a hidden volume password", - }, - self.model, - self.serial, - ) - .into() - } - - fn min_len(&self) -> u8 { - // More or less arbitrary minimum length based on the fact that the - // manual mentions six letter passwords in examples. Users - // *probably* should go longer than that, but we don't want to be - // too opinionated. - 6 - } -} - -/// Secret entry mode for pinentry. -/// -/// This enum describes the context of the pinentry query, for example -/// prompting for the current secret or requesting a new one. The mode -/// may affect the pinentry description and whether a quality bar is -/// shown. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Mode { - /// Let the user choose a new secret. - Choose, - /// Let the user confirm the previously chosen secret. - Confirm, - /// Query an existing secret. - Query, -} - -impl Mode { - fn show_quality_bar(self) -> bool { - self == Mode::Choose - } -} - -fn parse_pinentry_pin(response: R) -> crate::Result -where - R: AsRef, -{ - let string = response.as_ref(); - let lines: Vec<&str> = string.lines().collect(); - - // We expect the response to be of the form: - // > D passphrase - // > OK - // or potentially: - // > ERR 83886179 Operation cancelled - if lines.len() == 2 && lines[1] == "OK" && lines[0].starts_with("D ") { - // We got the only valid answer we accept. - let (_, pass) = lines[0].split_at(2); - return Ok(pass.to_string()); - } - - // Check if we are dealing with a special "ERR " line and report that - // specially. - if !lines.is_empty() && lines[0].starts_with("ERR ") { - let (_, error) = lines[0].split_at(4); - return Err(Error::from(error)); - } - Err(Error::Error(format!("Unexpected response: {}", string))) -} - -/// Inquire a secret from the user. -/// -/// This function inquires a secret from the user or returns a cached -/// entry, if available (and if caching is not disabled for the given -/// execution context). If an error message is set, it is displayed in -/// the entry dialog. The mode describes the context of the pinentry -/// dialog. It is used to choose an appropriate description and to -/// decide whether a quality bar is shown in the dialog. -pub fn inquire( - ctx: &mut args::ExecCtx<'_>, - entry: &E, - mode: Mode, - error_msg: Option<&str>, -) -> crate::Result -where - E: SecretEntry, -{ - let cache_id = entry - .cache_id() - .and_then(|id| if ctx.no_cache { None } else { Some(id) }) - // "X" is a sentinel value indicating that no caching is desired. - .unwrap_or_else(|| "X".into()) - .into(); - - let error_msg = error_msg - .map(|msg| msg.replace(" ", "+")) - .unwrap_or_else(|| String::from("+")); - let prompt = entry.prompt().replace(" ", "+"); - let description = entry.description(mode).replace(" ", "+"); - - let args = vec![cache_id, error_msg, prompt, description].join(" "); - let mut command = "GET_PASSPHRASE --data ".to_string(); - if mode.show_quality_bar() { - command += "--qualitybar "; - } - command += &args; - // An error reported for the GET_PASSPHRASE command does not actually - // cause gpg-connect-agent to exit with a non-zero error code, we have - // to evaluate the output to determine success/failure. - let output = process::Command::new("gpg-connect-agent") - .arg(command) - .arg("/bye") - .output() - .map_err(|err| match err.kind() { - io::ErrorKind::NotFound => { - io::Error::new(io::ErrorKind::NotFound, "gpg-connect-agent not found") - } - _ => err, - })?; - parse_pinentry_pin(str::from_utf8(&output.stdout)?) -} - -fn check(entry: &E, secret: &str) -> crate::Result<()> -where - E: SecretEntry, -{ - if secret.len() < usize::from(entry.min_len()) { - Err(Error::Error(format!( - "The secret must be at least {} characters long", - entry.min_len() - ))) - } else { - Ok(()) - } -} - -pub fn choose(ctx: &mut args::ExecCtx<'_>, entry: &E) -> crate::Result -where - E: SecretEntry, -{ - clear(entry)?; - let chosen = inquire(ctx, entry, Mode::Choose, None)?; - clear(entry)?; - check(entry, &chosen)?; - - let confirmed = inquire(ctx, entry, Mode::Confirm, None)?; - clear(entry)?; - - if chosen != confirmed { - Err(Error::from("Entered secrets do not match")) - } else { - Ok(chosen) - } -} - -fn parse_pinentry_response(response: R) -> crate::Result<()> -where - R: AsRef, -{ - let string = response.as_ref(); - let lines = string.lines().collect::>(); - - if lines.len() == 1 && lines[0] == "OK" { - // We got the only valid answer we accept. - return Ok(()); - } - Err(Error::Error(format!("Unexpected response: {}", string))) -} - -/// Clear the cached secret represented by the given entry. -pub fn clear(entry: &E) -> crate::Result<()> -where - E: SecretEntry, -{ - if let Some(cache_id) = entry.cache_id() { - let command = format!("CLEAR_PASSPHRASE {}", cache_id); - let output = process::Command::new("gpg-connect-agent") - .arg(command) - .arg("/bye") - .output()?; - - parse_pinentry_response(str::from_utf8(&output.stdout)?) - } else { - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_pinentry_pin_good() { - let response = "D passphrase\nOK\n"; - let expected = "passphrase"; - - assert_eq!(parse_pinentry_pin(response).unwrap(), expected) - } - - #[test] - fn parse_pinentry_pin_error() { - let error = "83886179 Operation cancelled"; - let response = "ERR ".to_string() + error + "\n"; - let expected = error; - - let error = parse_pinentry_pin(response); - - if let Error::Error(ref e) = error.err().unwrap() { - assert_eq!(e, &expected); - } else { - panic!("Unexpected result"); - } - } - - #[test] - fn parse_pinentry_pin_unexpected() { - let response = "foobar\n"; - let expected = format!("Unexpected response: {}", response); - let error = parse_pinentry_pin(response); - - if let Error::Error(ref e) = error.err().unwrap() { - assert_eq!(e, &expected); - } else { - panic!("Unexpected result"); - } - } - - #[test] - fn parse_pinentry_response_ok() { - assert!(parse_pinentry_response("OK\n").is_ok()) - } - - #[test] - fn parse_pinentry_response_ok_no_newline() { - assert!(parse_pinentry_response("OK").is_ok()) - } - - #[test] - fn parse_pinentry_response_unexpected() { - let response = "ERR 42"; - let expected = format!("Unexpected response: {}", response); - let error = parse_pinentry_response(response); - - if let Error::Error(ref e) = error.err().unwrap() { - assert_eq!(e, &expected); - } else { - panic!("Unexpected result"); - } - } -} diff --git a/nitrocli/src/redefine.rs b/nitrocli/src/redefine.rs deleted file mode 100644 index a79cb4b..0000000 --- a/nitrocli/src/redefine.rs +++ /dev/null @@ -1,38 +0,0 @@ -// redefine.rs - -// ************************************************************************* -// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -// A replacement of the standard println!() macro that requires an -// execution context as the first argument and prints to its stdout. -macro_rules! println { - ($ctx:expr) => { - writeln!($ctx.stdout, "") - }; - ($ctx:expr, $($arg:tt)*) => { - writeln!($ctx.stdout, $($arg)*) - }; -} - -macro_rules! eprintln { - ($ctx:expr) => { - writeln!($ctx.stderr, "") - }; - ($ctx:expr, $($arg:tt)*) => { - writeln!($ctx.stderr, $($arg)*) - }; -} diff --git a/nitrocli/src/tests/config.rs b/nitrocli/src/tests/config.rs deleted file mode 100644 index ea3a0e8..0000000 --- a/nitrocli/src/tests/config.rs +++ /dev/null @@ -1,66 +0,0 @@ -// config.rs - -// ************************************************************************* -// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use super::*; - -#[test_device] -fn get(model: nitrokey::Model) -> crate::Result<()> { - let re = regex::Regex::new( - r#"^Config: - numlock binding: (not set|\d+) - capslock binding: (not set|\d+) - scrollock binding: (not set|\d+) - require user PIN for OTP: (true|false) -$"#, - ) - .unwrap(); - - let out = Nitrocli::with_model(model).handle(&["config", "get"])?; - assert!(re.is_match(&out), out); - Ok(()) -} - -#[test_device] -fn set_wrong_usage(model: nitrokey::Model) { - let res = Nitrocli::with_model(model).handle(&["config", "set", "--numlock", "2", "-N"]); - assert_eq!( - res.unwrap_str_err(), - "--numlock and --no-numlock are mutually exclusive" - ); -} - -#[test_device] -fn set_get(model: nitrokey::Model) -> crate::Result<()> { - let mut ncli = Nitrocli::with_model(model); - let _ = ncli.handle(&["config", "set", "-s", "1", "-c", "0", "-N"])?; - - let re = regex::Regex::new( - r#"^Config: - numlock binding: not set - capslock binding: 0 - scrollock binding: 1 - require user PIN for OTP: (true|false) -$"#, - ) - .unwrap(); - - let out = ncli.handle(&["config", "get"])?; - assert!(re.is_match(&out), out); - Ok(()) -} diff --git a/nitrocli/src/tests/encrypted.rs b/nitrocli/src/tests/encrypted.rs deleted file mode 100644 index 75b84c3..0000000 --- a/nitrocli/src/tests/encrypted.rs +++ /dev/null @@ -1,95 +0,0 @@ -// encrypted.rs - -// ************************************************************************* -// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use super::*; - -#[test_device(storage)] -fn status_open_close(model: nitrokey::Model) -> crate::Result<()> { - fn make_re(open: Option) -> regex::Regex { - let encrypted = match open { - Some(open) => { - if open { - "active" - } else { - "(read-only|inactive)" - } - } - None => "(read-only|active|inactive)", - }; - let re = format!( - r#" - volumes: - unencrypted: (read-only|active|inactive) - encrypted: {} - hidden: (read-only|active|inactive) -$"#, - encrypted - ); - regex::Regex::new(&re).unwrap() - } - - let mut ncli = Nitrocli::with_model(model); - let out = ncli.handle(&["status"])?; - assert!(make_re(None).is_match(&out), out); - - let _ = ncli.handle(&["encrypted", "open"])?; - let out = ncli.handle(&["status"])?; - assert!(make_re(Some(true)).is_match(&out), out); - - let _ = ncli.handle(&["encrypted", "close"])?; - let out = ncli.handle(&["status"])?; - assert!(make_re(Some(false)).is_match(&out), out); - - Ok(()) -} - -#[test_device(pro)] -fn encrypted_open_on_pro(model: nitrokey::Model) { - let res = Nitrocli::with_model(model).handle(&["encrypted", "open"]); - assert_eq!( - res.unwrap_str_err(), - "This command is only available on the Nitrokey Storage", - ); -} - -#[test_device(storage)] -fn encrypted_open_close(model: nitrokey::Model) -> crate::Result<()> { - let mut ncli = Nitrocli::with_model(model); - let out = ncli.handle(&["encrypted", "open"])?; - assert!(out.is_empty()); - - { - let mut manager = nitrokey::force_take()?; - let device = manager.connect_storage()?; - assert!(device.get_status()?.encrypted_volume.active); - assert!(!device.get_status()?.hidden_volume.active); - } - - let out = ncli.handle(&["encrypted", "close"])?; - assert!(out.is_empty()); - - { - let mut manager = nitrokey::force_take()?; - let device = manager.connect_storage()?; - assert!(!device.get_status()?.encrypted_volume.active); - assert!(!device.get_status()?.hidden_volume.active); - } - - Ok(()) -} diff --git a/nitrocli/src/tests/hidden.rs b/nitrocli/src/tests/hidden.rs deleted file mode 100644 index 28a5d23..0000000 --- a/nitrocli/src/tests/hidden.rs +++ /dev/null @@ -1,49 +0,0 @@ -// hidden.rs - -// ************************************************************************* -// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use super::*; - -#[test_device(storage)] -fn hidden_create_open_close(model: nitrokey::Model) -> crate::Result<()> { - let mut ncli = Nitrocli::with_model(model); - let out = ncli.handle(&["hidden", "create", "0", "50", "100"])?; - assert!(out.is_empty()); - - let out = ncli.handle(&["hidden", "open"])?; - assert!(out.is_empty()); - - { - let mut manager = nitrokey::force_take()?; - let device = manager.connect_storage()?; - assert!(!device.get_status()?.encrypted_volume.active); - assert!(device.get_status()?.hidden_volume.active); - } - - let out = ncli.handle(&["hidden", "close"])?; - assert!(out.is_empty()); - - { - let mut manager = nitrokey::force_take()?; - let device = manager.connect_storage()?; - assert!(!device.get_status()?.encrypted_volume.active); - assert!(!device.get_status()?.hidden_volume.active); - } - - Ok(()) -} diff --git a/nitrocli/src/tests/lock.rs b/nitrocli/src/tests/lock.rs deleted file mode 100644 index 5140152..0000000 --- a/nitrocli/src/tests/lock.rs +++ /dev/null @@ -1,44 +0,0 @@ -// lock.rs - -// ************************************************************************* -// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use super::*; - -#[test_device(pro)] -fn lock_pro(model: nitrokey::Model) -> crate::Result<()> { - // We can't really test much more here than just success of the command. - let out = Nitrocli::with_model(model).handle(&["lock"])?; - assert!(out.is_empty()); - - Ok(()) -} - -#[test_device(storage)] -fn lock_storage(model: nitrokey::Model) -> crate::Result<()> { - let mut ncli = Nitrocli::with_model(model); - let _ = ncli.handle(&["encrypted", "open"])?; - - let out = ncli.handle(&["lock"])?; - assert!(out.is_empty()); - - let mut manager = nitrokey::force_take()?; - let device = manager.connect_storage()?; - assert!(!device.get_status()?.encrypted_volume.active); - - Ok(()) -} diff --git a/nitrocli/src/tests/mod.rs b/nitrocli/src/tests/mod.rs deleted file mode 100644 index 5ebf285..0000000 --- a/nitrocli/src/tests/mod.rs +++ /dev/null @@ -1,180 +0,0 @@ -// mod.rs - -// ************************************************************************* -// * Copyright (C) 2019-2020 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use std::ffi; -use std::fmt; - -use nitrokey_test::test as test_device; - -mod config; -mod encrypted; -mod hidden; -mod lock; -mod otp; -mod pin; -mod pws; -mod reset; -mod run; -mod status; -mod unencrypted; - -/// A trait simplifying checking for expected errors. -pub trait UnwrapError { - /// Unwrap an Error::Error variant. - fn unwrap_str_err(self) -> String; - /// Unwrap a Error::CommandError variant. - fn unwrap_cmd_err(self) -> (Option<&'static str>, nitrokey::CommandError); - /// Unwrap a Error::LibraryError variant. - fn unwrap_lib_err(self) -> (Option<&'static str>, nitrokey::LibraryError); -} - -impl UnwrapError for crate::Result -where - T: fmt::Debug, -{ - fn unwrap_str_err(self) -> String { - match self.unwrap_err() { - crate::Error::Error(err) => err, - err => panic!("Unexpected error variant found: {:?}", err), - } - } - - fn unwrap_cmd_err(self) -> (Option<&'static str>, nitrokey::CommandError) { - match self.unwrap_err() { - crate::Error::NitrokeyError(ctx, err) => match err { - nitrokey::Error::CommandError(err) => (ctx, err), - err => panic!("Unexpected error variant found: {:?}", err), - }, - err => panic!("Unexpected error variant found: {:?}", err), - } - } - - fn unwrap_lib_err(self) -> (Option<&'static str>, nitrokey::LibraryError) { - match self.unwrap_err() { - crate::Error::NitrokeyError(ctx, err) => match err { - nitrokey::Error::LibraryError(err) => (ctx, err), - err => panic!("Unexpected error variant found: {:?}", err), - }, - err => panic!("Unexpected error variant found: {:?}", err), - } - } -} - -struct Nitrocli { - model: Option, - admin_pin: Option, - user_pin: Option, - new_admin_pin: Option, - new_user_pin: Option, - password: Option, -} - -impl Nitrocli { - pub fn new() -> Self { - Self { - model: None, - admin_pin: Some(nitrokey::DEFAULT_ADMIN_PIN.into()), - user_pin: Some(nitrokey::DEFAULT_USER_PIN.into()), - new_admin_pin: None, - new_user_pin: None, - password: None, - } - } - - pub fn with_model(model: M) -> Self - where - M: Into, - { - Self { - model: Some(model.into()), - admin_pin: Some(nitrokey::DEFAULT_ADMIN_PIN.into()), - user_pin: Some(nitrokey::DEFAULT_USER_PIN.into()), - new_admin_pin: None, - new_user_pin: None, - password: Some("1234567".into()), - } - } - - pub fn admin_pin(&mut self, pin: impl Into) { - self.admin_pin = Some(pin.into()) - } - - pub fn new_admin_pin(&mut self, pin: impl Into) { - self.new_admin_pin = Some(pin.into()) - } - - pub fn user_pin(&mut self, pin: impl Into) { - self.user_pin = Some(pin.into()) - } - - pub fn new_user_pin(&mut self, pin: impl Into) { - self.new_user_pin = Some(pin.into()) - } - - fn model_to_arg(model: nitrokey::Model) -> &'static str { - match model { - nitrokey::Model::Pro => "--model=pro", - nitrokey::Model::Storage => "--model=storage", - } - } - - fn do_run(&mut self, args: &[&str], f: F) -> (R, Vec, Vec) - where - F: FnOnce(&mut crate::RunCtx<'_>, Vec) -> R, - { - let args = ["nitrocli"] - .iter() - .cloned() - .chain(self.model.map(Self::model_to_arg)) - .chain(args.iter().cloned()) - .map(ToOwned::to_owned) - .collect(); - - let mut stdout = Vec::new(); - let mut stderr = Vec::new(); - - let ctx = &mut crate::RunCtx { - stdout: &mut stdout, - stderr: &mut stderr, - admin_pin: self.admin_pin.clone(), - user_pin: self.user_pin.clone(), - new_admin_pin: self.new_admin_pin.clone(), - new_user_pin: self.new_user_pin.clone(), - password: self.password.clone(), - no_cache: true, - }; - - (f(ctx, args), stdout, stderr) - } - - /// Run `nitrocli`'s `run` function. - pub fn run(&mut self, args: &[&str]) -> (i32, Vec, Vec) { - self.do_run(args, |c, a| crate::run(c, a)) - } - - /// Run `nitrocli`'s `handle_arguments` function. - pub fn handle(&mut self, args: &[&str]) -> crate::Result { - let (res, out, _) = self.do_run(args, |c, a| crate::args::handle_arguments(c, a)); - res.map(|_| String::from_utf8_lossy(&out).into_owned()) - } - - pub fn model(&self) -> Option { - self.model - } -} diff --git a/nitrocli/src/tests/otp.rs b/nitrocli/src/tests/otp.rs deleted file mode 100644 index 0ccecf9..0000000 --- a/nitrocli/src/tests/otp.rs +++ /dev/null @@ -1,130 +0,0 @@ -// otp.rs - -// ************************************************************************* -// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use super::*; - -use crate::args; - -#[test_device] -fn set_invalid_slot_raw(model: nitrokey::Model) { - let (rc, out, err) = Nitrocli::with_model(model).run(&["otp", "set", "100", "name", "1234"]); - - assert_ne!(rc, 0); - assert_eq!(out, b""); - assert_eq!(&err[..24], b"Could not write OTP slot"); -} - -#[test_device] -fn set_invalid_slot(model: nitrokey::Model) { - let res = Nitrocli::with_model(model).handle(&["otp", "set", "100", "name", "1234"]); - - assert_eq!( - res.unwrap_lib_err(), - ( - Some("Could not write OTP slot"), - nitrokey::LibraryError::InvalidSlot - ) - ); -} - -#[test_device] -fn status(model: nitrokey::Model) -> crate::Result<()> { - let re = regex::Regex::new( - r#"^alg\tslot\tname -((totp|hotp)\t\d+\t.+\n)+$"#, - ) - .unwrap(); - - let mut ncli = Nitrocli::with_model(model); - // Make sure that we have at least something to display by ensuring - // that there is one slot programmed. - let _ = ncli.handle(&["otp", "set", "0", "the-name", "123456"])?; - - let out = ncli.handle(&["otp", "status"])?; - assert!(re.is_match(&out), out); - Ok(()) -} - -#[test_device] -fn set_get_hotp(model: nitrokey::Model) -> crate::Result<()> { - // Secret and expected HOTP values as per RFC 4226: Appendix D -- HOTP - // Algorithm: Test Values. - const SECRET: &str = "12345678901234567890"; - const OTP1: &str = concat!(755224, "\n"); - const OTP2: &str = concat!(287082, "\n"); - - let mut ncli = Nitrocli::with_model(model); - let _ = ncli.handle(&[ - "otp", "set", "-a", "hotp", "-f", "ascii", "1", "name", &SECRET, - ])?; - - let out = ncli.handle(&["otp", "get", "-a", "hotp", "1"])?; - assert_eq!(out, OTP1); - - let out = ncli.handle(&["otp", "get", "-a", "hotp", "1"])?; - assert_eq!(out, OTP2); - Ok(()) -} - -#[test_device] -fn set_get_totp(model: nitrokey::Model) -> crate::Result<()> { - // Secret and expected TOTP values as per RFC 6238: Appendix B -- - // Test Vectors. - const SECRET: &str = "12345678901234567890"; - const TIME: &str = stringify!(1111111111); - const OTP: &str = concat!(14050471, "\n"); - - let mut ncli = Nitrocli::with_model(model); - let _ = ncli.handle(&["otp", "set", "-d", "8", "-f", "ascii", "2", "name", &SECRET])?; - - let out = ncli.handle(&["otp", "get", "-t", TIME, "2"])?; - assert_eq!(out, OTP); - Ok(()) -} - -#[test_device] -fn set_totp_uneven_chars(model: nitrokey::Model) -> crate::Result<()> { - let secrets = [ - (args::OtpSecretFormat::Hex, "123"), - (args::OtpSecretFormat::Base32, "FBILDWWGA2"), - ]; - - for (format, secret) in &secrets { - let mut ncli = Nitrocli::with_model(model); - let _ = ncli.handle(&["otp", "set", "-f", format.as_ref(), "3", "foobar", &secret])?; - } - Ok(()) -} - -#[test_device] -fn clear(model: nitrokey::Model) -> crate::Result<()> { - let mut ncli = Nitrocli::with_model(model); - let _ = ncli.handle(&["otp", "set", "3", "hotp-test", "abcdef"])?; - let _ = ncli.handle(&["otp", "clear", "3"])?; - let res = ncli.handle(&["otp", "get", "3"]); - - assert_eq!( - res.unwrap_cmd_err(), - ( - Some("Could not generate OTP"), - nitrokey::CommandError::SlotNotProgrammed - ) - ); - Ok(()) -} diff --git a/nitrocli/src/tests/pin.rs b/nitrocli/src/tests/pin.rs deleted file mode 100644 index 958a36d..0000000 --- a/nitrocli/src/tests/pin.rs +++ /dev/null @@ -1,84 +0,0 @@ -// pin.rs - -// ************************************************************************* -// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use nitrokey::Authenticate; -use nitrokey::Device; - -use super::*; - -#[test_device] -fn unblock(model: nitrokey::Model) -> crate::Result<()> { - { - let mut manager = nitrokey::force_take()?; - let device = manager.connect_model(model)?; - let (device, err) = device.authenticate_user("wrong-pin").unwrap_err(); - match err { - nitrokey::Error::CommandError(err) if err == nitrokey::CommandError::WrongPassword => (), - _ => panic!("Unexpected error variant found: {:?}", err), - } - assert!(device.get_user_retry_count()? < 3); - } - - let _ = Nitrocli::with_model(model).handle(&["pin", "unblock"])?; - - { - let mut manager = nitrokey::force_take()?; - let device = manager.connect_model(model)?; - assert_eq!(device.get_user_retry_count()?, 3); - } - Ok(()) -} - -#[test_device] -fn set_user(model: nitrokey::Model) -> crate::Result<()> { - let mut ncli = Nitrocli::with_model(model); - // Set a new user PIN. - ncli.new_user_pin("new-pin"); - let out = ncli.handle(&["pin", "set", "user"])?; - assert!(out.is_empty()); - - { - let mut manager = nitrokey::force_take()?; - let device = manager.connect_model(model)?; - let (_, err) = device - .authenticate_user(nitrokey::DEFAULT_USER_PIN) - .unwrap_err(); - - match err { - nitrokey::Error::CommandError(err) if err == nitrokey::CommandError::WrongPassword => (), - _ => panic!("Unexpected error variant found: {:?}", err), - } - } - - // Revert to the default user PIN. - ncli.user_pin("new-pin"); - ncli.new_user_pin(nitrokey::DEFAULT_USER_PIN); - - let out = ncli.handle(&["pin", "set", "user"])?; - assert!(out.is_empty()); - - { - let mut manager = nitrokey::force_take()?; - let device = manager.connect_model(ncli.model().unwrap())?; - let _ = device - .authenticate_user(nitrokey::DEFAULT_USER_PIN) - .unwrap(); - } - Ok(()) -} diff --git a/nitrocli/src/tests/pws.rs b/nitrocli/src/tests/pws.rs deleted file mode 100644 index 651b2d5..0000000 --- a/nitrocli/src/tests/pws.rs +++ /dev/null @@ -1,123 +0,0 @@ -// pws.rs - -// ************************************************************************* -// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use super::*; - -#[test_device] -fn set_invalid_slot(model: nitrokey::Model) { - let res = Nitrocli::with_model(model).handle(&["pws", "set", "100", "name", "login", "1234"]); - - assert_eq!( - res.unwrap_lib_err(), - ( - Some("Could not write PWS slot"), - nitrokey::LibraryError::InvalidSlot - ) - ); -} - -#[test_device] -fn status(model: nitrokey::Model) -> crate::Result<()> { - let re = regex::Regex::new( - r#"^slot\tname -(\d+\t.+\n)+$"#, - ) - .unwrap(); - - let mut ncli = Nitrocli::with_model(model); - // Make sure that we have at least something to display by ensuring - // that there are there is one slot programmed. - let _ = ncli.handle(&["pws", "set", "0", "the-name", "the-login", "123456"])?; - - let out = ncli.handle(&["pws", "status"])?; - assert!(re.is_match(&out), out); - Ok(()) -} - -#[test_device] -fn set_get(model: nitrokey::Model) -> crate::Result<()> { - const NAME: &str = "dropbox"; - const LOGIN: &str = "d-e-s-o"; - const PASSWORD: &str = "my-secret-password"; - - let mut ncli = Nitrocli::with_model(model); - let _ = ncli.handle(&["pws", "set", "1", &NAME, &LOGIN, &PASSWORD])?; - - let out = ncli.handle(&["pws", "get", "1", "--quiet", "--name"])?; - assert_eq!(out, format!("{}\n", NAME)); - - let out = ncli.handle(&["pws", "get", "1", "--quiet", "--login"])?; - assert_eq!(out, format!("{}\n", LOGIN)); - - let out = ncli.handle(&["pws", "get", "1", "--quiet", "--password"])?; - assert_eq!(out, format!("{}\n", PASSWORD)); - - let out = ncli.handle(&["pws", "get", "1", "--quiet"])?; - assert_eq!(out, format!("{}\n{}\n{}\n", NAME, LOGIN, PASSWORD)); - - let out = ncli.handle(&["pws", "get", "1"])?; - assert_eq!( - out, - format!( - "name: {}\nlogin: {}\npassword: {}\n", - NAME, LOGIN, PASSWORD - ), - ); - Ok(()) -} - -#[test_device] -fn set_reset_get(model: nitrokey::Model) -> crate::Result<()> { - const NAME: &str = "some/svc"; - const LOGIN: &str = "a\\user"; - const PASSWORD: &str = "!@&-)*(&+%^@"; - - let mut ncli = Nitrocli::with_model(model); - let _ = ncli.handle(&["pws", "set", "2", &NAME, &LOGIN, &PASSWORD])?; - - let out = ncli.handle(&["reset"])?; - assert_eq!(out, ""); - - let res = ncli.handle(&["pws", "get", "2"]); - assert_eq!( - res.unwrap_cmd_err(), - ( - Some("Could not access PWS slot"), - nitrokey::CommandError::SlotNotProgrammed - ) - ); - Ok(()) -} - -#[test_device] -fn clear(model: nitrokey::Model) -> crate::Result<()> { - let mut ncli = Nitrocli::with_model(model); - let _ = ncli.handle(&["pws", "set", "10", "clear-test", "some-login", "abcdef"])?; - let _ = ncli.handle(&["pws", "clear", "10"])?; - let res = ncli.handle(&["pws", "get", "10"]); - - assert_eq!( - res.unwrap_cmd_err(), - ( - Some("Could not access PWS slot"), - nitrokey::CommandError::SlotNotProgrammed - ) - ); - Ok(()) -} diff --git a/nitrocli/src/tests/reset.rs b/nitrocli/src/tests/reset.rs deleted file mode 100644 index e197970..0000000 --- a/nitrocli/src/tests/reset.rs +++ /dev/null @@ -1,60 +0,0 @@ -// reset.rs - -// ************************************************************************* -// * Copyright (C) 2019 Robin Krahl (robin.krahl@ireas.org) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use nitrokey::Authenticate; -use nitrokey::GetPasswordSafe; - -use super::*; - -#[test_device] -fn reset(model: nitrokey::Model) -> crate::Result<()> { - let new_admin_pin = "87654321"; - let mut ncli = Nitrocli::with_model(model); - - // Change the admin PIN. - ncli.new_admin_pin(new_admin_pin); - let _ = ncli.handle(&["pin", "set", "admin"])?; - - { - let mut manager = nitrokey::force_take()?; - // Check that the admin PIN has been changed. - let device = manager.connect_model(ncli.model().unwrap())?; - let _ = device.authenticate_admin(new_admin_pin).unwrap(); - } - - // Perform factory reset - ncli.admin_pin(new_admin_pin); - let out = ncli.handle(&["reset"])?; - assert!(out.is_empty()); - - { - let mut manager = nitrokey::force_take()?; - // Check that the admin PIN has been reset. - let device = manager.connect_model(ncli.model().unwrap())?; - let mut device = device - .authenticate_admin(nitrokey::DEFAULT_ADMIN_PIN) - .unwrap(); - - // Check that the password store works, i.e., the AES key has been - // built. - let _ = device.get_password_safe(nitrokey::DEFAULT_USER_PIN)?; - } - - Ok(()) -} diff --git a/nitrocli/src/tests/run.rs b/nitrocli/src/tests/run.rs deleted file mode 100644 index c59c660..0000000 --- a/nitrocli/src/tests/run.rs +++ /dev/null @@ -1,103 +0,0 @@ -// run.rs - -// ************************************************************************* -// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use super::*; - -#[test] -fn no_command_or_option() { - let (rc, out, err) = Nitrocli::new().run(&[]); - - assert_ne!(rc, 0); - assert_eq!(out, b""); - - let s = String::from_utf8_lossy(&err).into_owned(); - assert!(s.starts_with("Usage:\n"), s); -} - -#[test] -fn help_options() { - fn test_run(args: &[&str], help: &str) { - let mut all = args.to_vec(); - all.push(help); - - let (rc, out, err) = Nitrocli::new().run(&all); - - assert_eq!(rc, 0); - assert_eq!(err, b""); - - let s = String::from_utf8_lossy(&out).into_owned(); - let expected = format!("Usage:\n nitrocli {}", args.join(" ")); - assert!(s.starts_with(&expected), s); - } - - fn test(args: &[&str]) { - test_run(args, "--help"); - test_run(args, "-h"); - } - - test(&[]); - test(&["config"]); - test(&["config", "get"]); - test(&["config", "set"]); - test(&["encrypted"]); - test(&["encrypted", "open"]); - test(&["encrypted", "close"]); - test(&["hidden"]); - test(&["hidden", "close"]); - test(&["hidden", "create"]); - test(&["hidden", "open"]); - test(&["lock"]); - test(&["otp"]); - test(&["otp", "clear"]); - test(&["otp", "get"]); - test(&["otp", "set"]); - test(&["otp", "status"]); - test(&["pin"]); - test(&["pin", "clear"]); - test(&["pin", "set"]); - test(&["pin", "unblock"]); - test(&["pws"]); - test(&["pws", "clear"]); - test(&["pws", "get"]); - test(&["pws", "set"]); - test(&["pws", "status"]); - test(&["reset"]); - test(&["status"]); - test(&["unencrypted"]); - test(&["unencrypted", "set"]); -} - -#[test] -fn version_option() { - fn test(re: ®ex::Regex, opt: &'static str) { - let (rc, out, err) = Nitrocli::new().run(&[opt]); - - assert_eq!(rc, 0); - assert_eq!(err, b""); - - let s = String::from_utf8_lossy(&out).into_owned(); - let _ = re; - assert!(re.is_match(&s), out); - } - - let re = regex::Regex::new(r"^nitrocli \d+.\d+.\d+(-[^-]+)*\n$").unwrap(); - - test(&re, "--version"); - test(&re, "-V"); -} diff --git a/nitrocli/src/tests/status.rs b/nitrocli/src/tests/status.rs deleted file mode 100644 index c9f4976..0000000 --- a/nitrocli/src/tests/status.rs +++ /dev/null @@ -1,81 +0,0 @@ -// status.rs - -// ************************************************************************* -// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use super::*; - -// This test acts as verification that conversion of Error::Error -// variants into the proper exit code works properly. -#[test_device] -fn not_found_raw() { - let (rc, out, err) = Nitrocli::new().run(&["status"]); - - assert_ne!(rc, 0); - assert_eq!(out, b""); - assert_eq!(err, b"Nitrokey device not found\n"); -} - -#[test_device] -fn not_found() { - let res = Nitrocli::new().handle(&["status"]); - assert_eq!(res.unwrap_str_err(), "Nitrokey device not found"); -} - -#[test_device(pro)] -fn output_pro(model: nitrokey::Model) -> crate::Result<()> { - let re = regex::Regex::new( - r#"^Status: - model: Pro - serial number: 0x[[:xdigit:]]{8} - firmware version: v\d+\.\d+ - user retry count: [0-3] - admin retry count: [0-3] -$"#, - ) - .unwrap(); - - let out = Nitrocli::with_model(model).handle(&["status"])?; - assert!(re.is_match(&out), out); - Ok(()) -} - -#[test_device(storage)] -fn output_storage(model: nitrokey::Model) -> crate::Result<()> { - let re = regex::Regex::new( - r#"^Status: - model: Storage - serial number: 0x[[:xdigit:]]{8} - firmware version: v\d+\.\d+ - user retry count: [0-3] - admin retry count: [0-3] - Storage: - SD card ID: 0x[[:xdigit:]]{8} - firmware: (un)?locked - storage keys: (not )?created - volumes: - unencrypted: (read-only|active|inactive) - encrypted: (read-only|active|inactive) - hidden: (read-only|active|inactive) -$"#, - ) - .unwrap(); - - let out = Nitrocli::with_model(model).handle(&["status"])?; - assert!(re.is_match(&out), out); - Ok(()) -} diff --git a/nitrocli/src/tests/unencrypted.rs b/nitrocli/src/tests/unencrypted.rs deleted file mode 100644 index 547dcaf..0000000 --- a/nitrocli/src/tests/unencrypted.rs +++ /dev/null @@ -1,46 +0,0 @@ -// unencrypted.rs - -// ************************************************************************* -// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see . * -// ************************************************************************* - -use super::*; - -#[test_device(storage)] -fn unencrypted_set_read_write(model: nitrokey::Model) -> crate::Result<()> { - let mut ncli = Nitrocli::with_model(model); - let out = ncli.handle(&["unencrypted", "set", "read-write"])?; - assert!(out.is_empty()); - - { - let mut manager = nitrokey::force_take()?; - let device = manager.connect_storage()?; - assert!(device.get_status()?.unencrypted_volume.active); - assert!(!device.get_status()?.unencrypted_volume.read_only); - } - - let out = ncli.handle(&["unencrypted", "set", "read-only"])?; - assert!(out.is_empty()); - - { - let mut manager = nitrokey::force_take()?; - let device = manager.connect_storage()?; - assert!(device.get_status()?.unencrypted_volume.active); - assert!(device.get_status()?.unencrypted_volume.read_only); - } - - Ok(()) -} diff --git a/nitrocli/var/binary-size.py b/nitrocli/var/binary-size.py deleted file mode 100755 index 3653814..0000000 --- a/nitrocli/var/binary-size.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/python3 -B - -#/*************************************************************************** -# * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * -# * * -# * This program is free software: you can redistribute it and/or modify * -# * it under the terms of the GNU General Public License as published by * -# * the Free Software Foundation, either version 3 of the License, or * -# * (at your option) any later version. * -# * * -# * This program is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU General Public License for more details. * -# * * -# * You should have received a copy of the GNU General Public License * -# * along with this program. If not, see . * -# ***************************************************************************/ - -from argparse import ( - ArgumentParser, - ArgumentTypeError, -) -from concurrent.futures import ( - ThreadPoolExecutor, -) -from json import ( - loads as jsonLoad, -) -from os import ( - stat, -) -from os.path import ( - join, -) -from subprocess import ( - check_call, - check_output, -) -from sys import ( - argv, - exit, -) -from tempfile import ( - TemporaryDirectory, -) - -UNITS = { - "byte": 1, - "kib": 1024, - "mib": 1024 * 1024, -} - -def unit(string): - """Create a unit.""" - if string in UNITS: - return UNITS[string] - else: - raise ArgumentTypeError("Invalid unit: \"%s\"." % string) - - -def nitrocliPath(cwd): - """Determine the path to the nitrocli release build binary.""" - out = check_output(["cargo", "metadata", "--format-version=1"], cwd=cwd) - data = jsonLoad(out) - return join(data["target_directory"], "release", "nitrocli") - - -def fileSize(path): - """Determine the size of the file at the given path.""" - return stat(path).st_size - - -def repoRoot(): - """Retrieve the root directory of the current git repository.""" - out = check_output(["git", "rev-parse", "--show-toplevel"]) - return out.decode().strip() - - -def resolveCommit(commit): - """Resolve a commit into a SHA1 hash.""" - out = check_output(["git", "rev-parse", "--verify", "%s^{commit}" % commit]) - return out.decode().strip() - - -def determineSizeAt(root, rev): - """Determine the size of the nitrocli release build binary at the given git revision.""" - sha1 = resolveCommit(rev) - with TemporaryDirectory() as d: - cwd = join(d, "nitrocli") - check_call(["git", "clone", root, d]) - check_call(["git", "checkout", "--quiet", sha1], cwd=cwd) - check_call(["cargo", "build", "--quiet", "--release"], cwd=cwd) - - ncli = nitrocliPath(cwd) - check_call(["strip", ncli]) - return fileSize(ncli) - - -def setupArgumentParser(): - """Create and initialize an argument parser.""" - parser = ArgumentParser() - parser.add_argument( - "revs", metavar="REVS", nargs="+", - help="The revisions at which to measure the release binary size.", - ) - parser.add_argument( - "-u", "--unit", default="byte", dest="unit", metavar="UNIT", type=unit, - help="The unit in which to output the result (%s)." % "|".join(UNITS.keys()), - ) - return parser - - -def main(args): - """Determine the size of the nitrocli binary at given git revisions.""" - parser = setupArgumentParser() - ns = parser.parse_args(args) - root = repoRoot() - futures = [] - executor = ThreadPoolExecutor() - - for rev in ns.revs: - futures += [executor.submit(lambda r=rev: determineSizeAt(root, r))] - - executor.shutdown(wait=True) - - for future in futures: - print(int(round(future.result() / ns.unit, 0))) - - return 0 - - -if __name__ == "__main__": - exit(main(argv[1:])) diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..b196eaa --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +tab_spaces = 2 diff --git a/src/arg_util.rs b/src/arg_util.rs new file mode 100644 index 0000000..e2e7b1d --- /dev/null +++ b/src/arg_util.rs @@ -0,0 +1,158 @@ +// arg_util.rs + +// ************************************************************************* +// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +macro_rules! count { + ($head:ident) => { 1 }; + ($head:ident, $($tail:ident),*) => { + 1 + count!($($tail),*) + } +} + +/// A macro for generating an enum with a set of simple (i.e., no +/// parameters) variants and their textual representations. +// TODO: Right now we hard code the derives we create. We may want to +// make this set configurable. +macro_rules! Enum { + ( $name:ident, [ $( $var:ident => ($str:expr, $exec:expr), ) *] ) => { + Enum! {$name, [ + $( $var => $str, )* + ]} + + #[allow(unused_qualifications)] + impl $name { + fn execute( + self, + ctx: &mut crate::args::ExecCtx<'_>, + args: ::std::vec::Vec<::std::string::String>, + ) -> crate::Result<()> { + match self { + $( + $name::$var => $exec(ctx, args), + )* + } + } + } + }; + ( $name:ident, [ $( $var:ident => $str:expr, ) *] ) => { + #[derive(Clone, Copy, Debug, PartialEq)] + pub enum $name { + $( + $var, + )* + } + + impl $name { + #[allow(unused)] + pub fn all(&self) -> [$name; count!($($var),*) ] { + $name::all_variants() + } + + pub fn all_variants() -> [$name; count!($($var),*) ] { + [ + $( + $name::$var, + )* + ] + } + } + + impl ::std::convert::AsRef for $name { + fn as_ref(&self) -> &'static str { + match *self { + $( + $name::$var => $str, + )* + } + } + } + + impl ::std::fmt::Display for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "{}", self.as_ref()) + } + } + + impl ::std::str::FromStr for $name { + type Err = (); + + fn from_str(s: &str) -> ::std::result::Result { + match s { + $( + $str => Ok($name::$var), + )* + _ => Err(()), + } + } + } + }; +} + +/// A macro for formatting the variants of an enum (as created by the +/// Enum!{} macro) ready to be used in a help text. The supplied `fmt` +/// needs to contain the named parameter `{variants}`, which will be +/// replaced with a generated version of the enum's variants. +macro_rules! fmt_enum { + ( $enm:ident ) => {{ + fmt_enum!($enm.all()) + }}; + ( $all:expr ) => {{ + $all + .iter() + .map(::std::convert::AsRef::as_ref) + .collect::<::std::vec::Vec<_>>() + .join("|") + }}; +} + +/// A macro for generating the help text for a command/subcommand. The +/// argument is the variable representing the command (which in turn is +/// an enum). +/// Note that the name of this variable is embedded into the help text! +macro_rules! cmd_help { + ( $cmd:ident ) => { + format!( + concat!("The ", stringify!($cmd), " to execute ({})"), + fmt_enum!($cmd) + ) + }; +} + +#[cfg(test)] +mod tests { + Enum! {Command, [ + Var1 => "var1", + Var2 => "2", + Var3 => "crazy", + ]} + + #[test] + fn all_variants() { + assert_eq!( + Command::all_variants(), + [Command::Var1, Command::Var2, Command::Var3] + ) + } + + #[test] + fn text_representations() { + assert_eq!(Command::Var1.as_ref(), "var1"); + assert_eq!(Command::Var2.as_ref(), "2"); + assert_eq!(Command::Var3.as_ref(), "crazy"); + } +} diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..9f4cae2 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,984 @@ +// args.rs + +// ************************************************************************* +// * Copyright (C) 2018-2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use std::ffi; +use std::io; +use std::result; +use std::str; + +use crate::commands; +use crate::error::Error; +use crate::pinentry; +use crate::RunCtx; + +type Result = result::Result; + +/// Wraps a writer and buffers its output. +/// +/// This implementation is similar to `io::BufWriter`, but: +/// - The inner writer is only written to if `flush` is called. +/// - The buffer may grow infinitely large. +struct BufWriter<'w, W: io::Write + ?Sized> { + buf: Vec, + inner: &'w mut W, +} + +impl<'w, W: io::Write + ?Sized> BufWriter<'w, W> { + pub fn new(inner: &'w mut W) -> Self { + BufWriter { + buf: Vec::with_capacity(128), + inner, + } + } +} + +impl<'w, W: io::Write + ?Sized> io::Write for BufWriter<'w, W> { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.buf.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + self.inner.write_all(&self.buf)?; + self.buf.clear(); + self.inner.flush() + } +} + +trait Stdio { + fn stdio(&mut self) -> (&mut dyn io::Write, &mut dyn io::Write); +} + +impl<'io> Stdio for RunCtx<'io> { + fn stdio(&mut self) -> (&mut dyn io::Write, &mut dyn io::Write) { + (self.stdout, self.stderr) + } +} + +impl Stdio for (&mut W, &mut W) +where + W: io::Write, +{ + fn stdio(&mut self) -> (&mut dyn io::Write, &mut dyn io::Write) { + (self.0, self.1) + } +} + +/// A command execution context that captures additional data pertaining +/// the command execution. +pub struct ExecCtx<'io> { + pub model: Option, + pub stdout: &'io mut dyn io::Write, + pub stderr: &'io mut dyn io::Write, + pub admin_pin: Option, + pub user_pin: Option, + pub new_admin_pin: Option, + pub new_user_pin: Option, + pub password: Option, + pub no_cache: bool, + pub verbosity: u64, +} + +impl<'io> Stdio for ExecCtx<'io> { + fn stdio(&mut self) -> (&mut dyn io::Write, &mut dyn io::Write) { + (self.stdout, self.stderr) + } +} + +/// The available Nitrokey models. +#[allow(unused_doc_comments)] +Enum! {DeviceModel, [ + Pro => "pro", + Storage => "storage", +]} + +impl DeviceModel { + pub fn as_user_facing_str(&self) -> &str { + match self { + DeviceModel::Pro => "Pro", + DeviceModel::Storage => "Storage", + } + } +} + +impl From for nitrokey::Model { + fn from(model: DeviceModel) -> nitrokey::Model { + match model { + DeviceModel::Pro => nitrokey::Model::Pro, + DeviceModel::Storage => nitrokey::Model::Storage, + } + } +} + +/// A top-level command for nitrocli. +#[allow(unused_doc_comments)] +Enum! {Command, [ + Config => ("config", config), + Encrypted => ("encrypted", encrypted), + Hidden => ("hidden", hidden), + Lock => ("lock", lock), + Otp => ("otp", otp), + Pin => ("pin", pin), + Pws => ("pws", pws), + Reset => ("reset", reset), + Status => ("status", status), + Unencrypted => ("unencrypted", unencrypted), +]} + +Enum! {ConfigCommand, [ + Get => ("get", config_get), + Set => ("set", config_set), +]} + +#[derive(Clone, Copy, Debug)] +pub enum ConfigOption { + Enable(T), + Disable, + Ignore, +} + +impl ConfigOption { + fn try_from(disable: bool, value: Option, name: &'static str) -> Result { + if disable { + if value.is_some() { + Err(Error::Error(format!( + "--{name} and --no-{name} are mutually exclusive", + name = name + ))) + } else { + Ok(ConfigOption::Disable) + } + } else { + match value { + Some(value) => Ok(ConfigOption::Enable(value)), + None => Ok(ConfigOption::Ignore), + } + } + } + + pub fn or(self, default: Option) -> Option { + match self { + ConfigOption::Enable(value) => Some(value), + ConfigOption::Disable => None, + ConfigOption::Ignore => default, + } + } +} + +Enum! {OtpCommand, [ + Clear => ("clear", otp_clear), + Get => ("get", otp_get), + Set => ("set", otp_set), + Status => ("status", otp_status), +]} + +Enum! {OtpAlgorithm, [ + Hotp => "hotp", + Totp => "totp", +]} + +Enum! {OtpMode, [ + SixDigits => "6", + EightDigits => "8", +]} + +impl From for nitrokey::OtpMode { + fn from(mode: OtpMode) -> Self { + match mode { + OtpMode::SixDigits => nitrokey::OtpMode::SixDigits, + OtpMode::EightDigits => nitrokey::OtpMode::EightDigits, + } + } +} + +Enum! {OtpSecretFormat, [ + Ascii => "ascii", + Base32 => "base32", + Hex => "hex", +]} + +Enum! {PinCommand, [ + Clear => ("clear", pin_clear), + Set => ("set", pin_set), + Unblock => ("unblock", pin_unblock), +]} + +Enum! {PwsCommand, [ + Clear => ("clear", pws_clear), + Get => ("get", pws_get), + Set => ("set", pws_set), + Status => ("status", pws_status), +]} + +fn parse( + ctx: &mut impl Stdio, + parser: argparse::ArgumentParser<'_>, + args: Vec, +) -> Result<()> { + let (stdout, stderr) = ctx.stdio(); + let result = parser + .parse(args, stdout, stderr) + .map_err(Error::ArgparseError); + drop(parser); + result +} + +/// Inquire the status of the Nitrokey. +fn status(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Prints the status of the connected Nitrokey device"); + parse(ctx, parser, args)?; + + commands::status(ctx) +} + +/// Perform a factory reset. +fn reset(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Performs a factory reset"); + parse(ctx, parser, args)?; + + commands::reset(ctx) +} + +Enum! {UnencryptedCommand, [ + Set => ("set", unencrypted_set), +]} + +Enum! {UnencryptedVolumeMode, [ + ReadWrite => "read-write", + ReadOnly => "read-only", +]} + +/// Execute an unencrypted subcommand. +fn unencrypted(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut subcommand = UnencryptedCommand::Set; + let help = cmd_help!(subcommand); + let mut subargs = vec![]; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Interacts with the device's unencrypted volume"); + let _ = + parser + .refer(&mut subcommand) + .required() + .add_argument("subcommand", argparse::Store, &help); + let _ = parser.refer(&mut subargs).add_argument( + "arguments", + argparse::List, + "The arguments for the subcommand", + ); + parser.stop_on_first_argument(true); + parse(ctx, parser, args)?; + + subargs.insert( + 0, + format!( + "{} {} {}", + crate::NITROCLI, + Command::Unencrypted, + subcommand, + ), + ); + subcommand.execute(ctx, subargs) +} + +/// Change the configuration of the unencrypted volume. +fn unencrypted_set(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut mode = UnencryptedVolumeMode::ReadWrite; + let help = format!("The mode to change to ({})", fmt_enum!(mode)); + let mut parser = argparse::ArgumentParser::new(); + parser + .set_description("Changes the configuration of the unencrypted volume on a Nitrokey Storage"); + let _ = parser + .refer(&mut mode) + .required() + .add_argument("type", argparse::Store, &help); + parse(ctx, parser, args)?; + + commands::unencrypted_set(ctx, mode) +} + +Enum! {EncryptedCommand, [ + Close => ("close", encrypted_close), + Open => ("open", encrypted_open), +]} + +/// Execute an encrypted subcommand. +fn encrypted(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut subcommand = EncryptedCommand::Open; + let help = cmd_help!(subcommand); + let mut subargs = vec![]; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Interacts with the device's encrypted volume"); + let _ = + parser + .refer(&mut subcommand) + .required() + .add_argument("subcommand", argparse::Store, &help); + let _ = parser.refer(&mut subargs).add_argument( + "arguments", + argparse::List, + "The arguments for the subcommand", + ); + parser.stop_on_first_argument(true); + parse(ctx, parser, args)?; + + subargs.insert( + 0, + format!("{} {} {}", crate::NITROCLI, Command::Encrypted, subcommand), + ); + subcommand.execute(ctx, subargs) +} + +/// Open the encrypted volume on the Nitrokey. +fn encrypted_open(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Opens the encrypted volume on a Nitrokey Storage"); + parse(ctx, parser, args)?; + + commands::encrypted_open(ctx) +} + +/// Close the previously opened encrypted volume. +fn encrypted_close(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Closes the encrypted volume on a Nitrokey Storage"); + parse(ctx, parser, args)?; + + commands::encrypted_close(ctx) +} + +Enum! {HiddenCommand, [ + Close => ("close", hidden_close), + Create => ("create", hidden_create), + Open => ("open", hidden_open), +]} + +/// Execute a hidden subcommand. +fn hidden(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut subcommand = HiddenCommand::Open; + let help = cmd_help!(subcommand); + let mut subargs = vec![]; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Interacts with the device's hidden volume"); + let _ = + parser + .refer(&mut subcommand) + .required() + .add_argument("subcommand", argparse::Store, &help); + let _ = parser.refer(&mut subargs).add_argument( + "arguments", + argparse::List, + "The arguments for the subcommand", + ); + parser.stop_on_first_argument(true); + parse(ctx, parser, args)?; + + subargs.insert( + 0, + format!("{} {} {}", crate::NITROCLI, Command::Hidden, subcommand), + ); + subcommand.execute(ctx, subargs) +} + +fn hidden_create(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut slot: u8 = 0; + let mut start: u8 = 0; + let mut end: u8 = 0; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Creates a hidden volume on a Nitrokey Storage"); + let _ = parser.refer(&mut slot).required().add_argument( + "slot", + argparse::Store, + "The hidden volume slot to use", + ); + let _ = parser.refer(&mut start).required().add_argument( + "start", + argparse::Store, + "The start location of the hidden volume as percentage of the \ + encrypted volume's size (0-99)", + ); + let _ = parser.refer(&mut end).required().add_argument( + "end", + argparse::Store, + "The end location of the hidden volume as percentage of the \ + encrypted volume's size (1-100)", + ); + parse(ctx, parser, args)?; + + commands::hidden_create(ctx, slot, start, end) +} + +fn hidden_open(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Opens a hidden volume on a Nitrokey Storage"); + parse(ctx, parser, args)?; + + commands::hidden_open(ctx) +} + +fn hidden_close(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Closes the hidden volume on a Nitrokey Storage"); + parse(ctx, parser, args)?; + + commands::hidden_close(ctx) +} + +/// Execute a config subcommand. +fn config(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut subcommand = ConfigCommand::Get; + let help = cmd_help!(subcommand); + let mut subargs = vec![]; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Reads or writes the device configuration"); + let _ = + parser + .refer(&mut subcommand) + .required() + .add_argument("subcommand", argparse::Store, &help); + let _ = parser.refer(&mut subargs).add_argument( + "arguments", + argparse::List, + "The arguments for the subcommand", + ); + parser.stop_on_first_argument(true); + parse(ctx, parser, args)?; + + subargs.insert( + 0, + format!("{} {} {}", crate::NITROCLI, Command::Config, subcommand), + ); + subcommand.execute(ctx, subargs) +} + +/// Read the Nitrokey configuration. +fn config_get(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Prints the Nitrokey configuration"); + parse(ctx, parser, args)?; + + commands::config_get(ctx) +} + +/// Write the Nitrokey configuration. +fn config_set(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut numlock = None; + let mut no_numlock = false; + let mut capslock = None; + let mut no_capslock = false; + let mut scrollock = None; + let mut no_scrollock = false; + let mut otp_pin = false; + let mut no_otp_pin = false; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Changes the Nitrokey configuration"); + let _ = parser.refer(&mut numlock).add_option( + &["-n", "--numlock"], + argparse::StoreOption, + "Set the numlock option to the given HOTP slot", + ); + let _ = parser.refer(&mut no_numlock).add_option( + &["-N", "--no-numlock"], + argparse::StoreTrue, + "Unset the numlock option", + ); + let _ = parser.refer(&mut capslock).add_option( + &["-c", "--capslock"], + argparse::StoreOption, + "Set the capslock option to the given HOTP slot", + ); + let _ = parser.refer(&mut no_capslock).add_option( + &["-C", "--no-capslock"], + argparse::StoreTrue, + "Unset the capslock option", + ); + let _ = parser.refer(&mut scrollock).add_option( + &["-s", "--scrollock"], + argparse::StoreOption, + "Set the scrollock option to the given HOTP slot", + ); + let _ = parser.refer(&mut no_scrollock).add_option( + &["-S", "--no-scrollock"], + argparse::StoreTrue, + "Unset the scrollock option", + ); + let _ = parser.refer(&mut otp_pin).add_option( + &["-o", "--otp-pin"], + argparse::StoreTrue, + "Require the user PIN to generate one-time passwords", + ); + let _ = parser.refer(&mut no_otp_pin).add_option( + &["-O", "--no-otp-pin"], + argparse::StoreTrue, + "Allow one-time password generation without PIN", + ); + parse(ctx, parser, args)?; + + let numlock = ConfigOption::try_from(no_numlock, numlock, "numlock")?; + let capslock = ConfigOption::try_from(no_capslock, capslock, "capslock")?; + let scrollock = ConfigOption::try_from(no_scrollock, scrollock, "scrollock")?; + let otp_pin = if otp_pin { + Some(true) + } else if no_otp_pin { + Some(false) + } else { + None + }; + commands::config_set(ctx, numlock, capslock, scrollock, otp_pin) +} + +/// Lock the Nitrokey. +fn lock(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Locks the connected Nitrokey device"); + parse(ctx, parser, args)?; + + commands::lock(ctx) +} + +/// Execute an OTP subcommand. +fn otp(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut subcommand = OtpCommand::Get; + let help = cmd_help!(subcommand); + let mut subargs = vec![]; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Accesses one-time passwords"); + let _ = + parser + .refer(&mut subcommand) + .required() + .add_argument("subcommand", argparse::Store, &help); + let _ = parser.refer(&mut subargs).add_argument( + "arguments", + argparse::List, + "The arguments for the subcommand", + ); + parser.stop_on_first_argument(true); + parse(ctx, parser, args)?; + + subargs.insert( + 0, + format!("{} {} {}", crate::NITROCLI, Command::Otp, subcommand), + ); + subcommand.execute(ctx, subargs) +} + +/// Generate a one-time password on the Nitrokey device. +fn otp_get(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut slot: u8 = 0; + let mut algorithm = OtpAlgorithm::Totp; + let help = format!( + "The OTP algorithm to use ({}, default: {})", + fmt_enum!(algorithm), + algorithm + ); + let mut time: Option = None; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Generates a one-time password"); + let _ = + parser + .refer(&mut slot) + .required() + .add_argument("slot", argparse::Store, "The OTP slot to use"); + let _ = parser + .refer(&mut algorithm) + .add_option(&["-a", "--algorithm"], argparse::Store, &help); + let _ = parser.refer(&mut time).add_option( + &["-t", "--time"], + argparse::StoreOption, + "The time to use for TOTP generation (Unix timestamp, default: system time)", + ); + parse(ctx, parser, args)?; + + commands::otp_get(ctx, slot, algorithm, time) +} + +/// Configure a one-time password slot on the Nitrokey device. +pub fn otp_set(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut slot: u8 = 0; + let mut algorithm = OtpAlgorithm::Totp; + let algo_help = format!( + "The OTP algorithm to use ({}, default: {})", + fmt_enum!(algorithm), + algorithm + ); + let mut name = "".to_owned(); + let mut secret = "".to_owned(); + let mut digits = OtpMode::SixDigits; + let mut counter: u64 = 0; + let mut time_window: u16 = 30; + let mut secret_format = OtpSecretFormat::Hex; + let fmt_help = format!( + "The format of the secret ({}, default: {})", + fmt_enum!(OtpSecretFormat::all_variants()), + secret_format, + ); + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Configures a one-time password slot"); + let _ = + parser + .refer(&mut slot) + .required() + .add_argument("slot", argparse::Store, "The OTP slot to use"); + let _ = + parser + .refer(&mut algorithm) + .add_option(&["-a", "--algorithm"], argparse::Store, &algo_help); + let _ = parser.refer(&mut name).required().add_argument( + "name", + argparse::Store, + "The name of the slot", + ); + let _ = parser.refer(&mut secret).required().add_argument( + "secret", + argparse::Store, + "The secret to store on the slot as a hexadecimal string (unless overwritten by --format)", + ); + let _ = parser.refer(&mut digits).add_option( + &["-d", "--digits"], + argparse::Store, + "The number of digits to use for the one-time password (6 or 8, default: 6)", + ); + let _ = parser.refer(&mut counter).add_option( + &["-c", "--counter"], + argparse::Store, + "The counter value for HOTP (default: 0)", + ); + let _ = parser.refer(&mut time_window).add_option( + &["-t", "--time-window"], + argparse::Store, + "The time window for TOTP (default: 30)", + ); + let _ = + parser + .refer(&mut secret_format) + .add_option(&["-f", "--format"], argparse::Store, &fmt_help); + parse(ctx, parser, args)?; + + let data = nitrokey::OtpSlotData { + number: slot, + name, + secret, + mode: nitrokey::OtpMode::from(digits), + use_enter: false, + token_id: None, + }; + commands::otp_set(ctx, data, algorithm, counter, time_window, secret_format) +} + +/// Clear an OTP slot. +fn otp_clear(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut slot: u8 = 0; + let mut algorithm = OtpAlgorithm::Totp; + let help = format!( + "The OTP algorithm to use ({}, default: {})", + fmt_enum!(algorithm), + algorithm + ); + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Clears a one-time password slot"); + let _ = parser.refer(&mut slot).required().add_argument( + "slot", + argparse::Store, + "The OTP slot to clear", + ); + let _ = parser + .refer(&mut algorithm) + .add_option(&["-a", "--algorithm"], argparse::Store, &help); + parse(ctx, parser, args)?; + + commands::otp_clear(ctx, slot, algorithm) +} + +/// Print the status of the OTP slots. +fn otp_status(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut all = false; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Prints the status of the OTP slots"); + let _ = parser.refer(&mut all).add_option( + &["-a", "--all"], + argparse::StoreTrue, + "Show slots that are not programmed", + ); + parse(ctx, parser, args)?; + + commands::otp_status(ctx, all) +} + +/// Execute a PIN subcommand. +fn pin(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut subcommand = PinCommand::Clear; + let help = cmd_help!(subcommand); + let mut subargs = vec![]; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Manages the Nitrokey PINs"); + let _ = + parser + .refer(&mut subcommand) + .required() + .add_argument("subcommand", argparse::Store, &help); + let _ = parser.refer(&mut subargs).add_argument( + "arguments", + argparse::List, + "The arguments for the subcommand", + ); + parser.stop_on_first_argument(true); + parse(ctx, parser, args)?; + + subargs.insert( + 0, + format!("{} {} {}", crate::NITROCLI, Command::Pin, subcommand), + ); + subcommand.execute(ctx, subargs) +} + +/// Clear the PIN as cached by various other commands. +fn pin_clear(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Clears the cached PINs"); + parse(ctx, parser, args)?; + + commands::pin_clear(ctx) +} + +/// Change a PIN. +fn pin_set(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut pintype = pinentry::PinType::User; + let help = format!("The PIN type to change ({})", fmt_enum!(pintype)); + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Changes a PIN"); + let _ = parser + .refer(&mut pintype) + .required() + .add_argument("type", argparse::Store, &help); + parse(ctx, parser, args)?; + + commands::pin_set(ctx, pintype) +} + +/// Unblock and reset the user PIN. +fn pin_unblock(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Unblocks and resets the user PIN"); + parse(ctx, parser, args)?; + + commands::pin_unblock(ctx) +} + +/// Execute a PWS subcommand. +fn pws(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut subcommand = PwsCommand::Get; + let mut subargs = vec![]; + let help = cmd_help!(subcommand); + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Accesses the password safe"); + let _ = + parser + .refer(&mut subcommand) + .required() + .add_argument("subcommand", argparse::Store, &help); + let _ = parser.refer(&mut subargs).add_argument( + "arguments", + argparse::List, + "The arguments for the subcommand", + ); + parser.stop_on_first_argument(true); + parse(ctx, parser, args)?; + + subargs.insert( + 0, + format!("{} {} {}", crate::NITROCLI, Command::Pws, subcommand), + ); + subcommand.execute(ctx, subargs) +} + +/// Access a slot of the password safe on the Nitrokey. +fn pws_get(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut slot: u8 = 0; + let mut name = false; + let mut login = false; + let mut password = false; + let mut quiet = false; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Reads a password safe slot"); + let _ = parser.refer(&mut slot).required().add_argument( + "slot", + argparse::Store, + "The PWS slot to read", + ); + let _ = parser.refer(&mut name).add_option( + &["-n", "--name"], + argparse::StoreTrue, + "Show the name stored on the slot", + ); + let _ = parser.refer(&mut login).add_option( + &["-l", "--login"], + argparse::StoreTrue, + "Show the login stored on the slot", + ); + let _ = parser.refer(&mut password).add_option( + &["-p", "--password"], + argparse::StoreTrue, + "Show the password stored on the slot", + ); + let _ = parser.refer(&mut quiet).add_option( + &["-q", "--quiet"], + argparse::StoreTrue, + "Print the stored data without description", + ); + parse(ctx, parser, args)?; + + commands::pws_get(ctx, slot, name, login, password, quiet) +} + +/// Set a slot of the password safe on the Nitrokey. +fn pws_set(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut slot: u8 = 0; + let mut name = String::new(); + let mut login = String::new(); + let mut password = String::new(); + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Writes a password safe slot"); + let _ = parser.refer(&mut slot).required().add_argument( + "slot", + argparse::Store, + "The PWS slot to write", + ); + let _ = parser.refer(&mut name).required().add_argument( + "name", + argparse::Store, + "The name to store on the slot", + ); + let _ = parser.refer(&mut login).required().add_argument( + "login", + argparse::Store, + "The login to store on the slot", + ); + let _ = parser.refer(&mut password).required().add_argument( + "password", + argparse::Store, + "The password to store on the slot", + ); + parse(ctx, parser, args)?; + + commands::pws_set(ctx, slot, &name, &login, &password) +} + +/// Clear a PWS slot. +fn pws_clear(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut slot: u8 = 0; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Clears a password safe slot"); + let _ = parser.refer(&mut slot).required().add_argument( + "slot", + argparse::Store, + "The PWS slot to clear", + ); + parse(ctx, parser, args)?; + + commands::pws_clear(ctx, slot) +} + +/// Print the status of the PWS slots. +fn pws_status(ctx: &mut ExecCtx<'_>, args: Vec) -> Result<()> { + let mut all = false; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Prints the status of the PWS slots"); + let _ = parser.refer(&mut all).add_option( + &["-a", "--all"], + argparse::StoreTrue, + "Show slots that are not programmed", + ); + parse(ctx, parser, args)?; + + commands::pws_status(ctx, all) +} + +/// Parse the command-line arguments and execute the selected command. +pub(crate) fn handle_arguments(ctx: &mut RunCtx<'_>, args: Vec) -> Result<()> { + use std::io::Write; + + let mut version = false; + let mut model: Option = None; + let model_help = format!( + "Select the device model to connect to ({})", + fmt_enum!(DeviceModel::all_variants()) + ); + let mut verbosity = 0; + let mut command = Command::Status; + let cmd_help = cmd_help!(command); + let mut subargs = vec![]; + let mut parser = argparse::ArgumentParser::new(); + let _ = parser.refer(&mut version).add_option( + &["-V", "--version"], + argparse::StoreTrue, + "Print version information and exit", + ); + let _ = parser.refer(&mut verbosity).add_option( + &["-v", "--verbose"], + argparse::IncrBy::(1), + "Increase the log level (can be supplied multiple times)", + ); + let _ = + parser + .refer(&mut model) + .add_option(&["-m", "--model"], argparse::StoreOption, &model_help); + parser.set_description("Provides access to a Nitrokey device"); + let _ = parser + .refer(&mut command) + .required() + .add_argument("command", argparse::Store, &cmd_help); + let _ = parser.refer(&mut subargs).add_argument( + "arguments", + argparse::List, + "The arguments for the command", + ); + parser.stop_on_first_argument(true); + + let mut stdout_buf = BufWriter::new(ctx.stdout); + let mut stderr_buf = BufWriter::new(ctx.stderr); + let mut stdio_buf = (&mut stdout_buf, &mut stderr_buf); + let result = parse(&mut stdio_buf, parser, args); + + if version { + println!(ctx, "{} {}", crate::NITROCLI, env!("CARGO_PKG_VERSION"))?; + Ok(()) + } else { + stdout_buf.flush()?; + stderr_buf.flush()?; + + result?; + subargs.insert(0, format!("{} {}", crate::NITROCLI, command)); + + let mut ctx = ExecCtx { + model, + stdout: ctx.stdout, + stderr: ctx.stderr, + admin_pin: ctx.admin_pin.take(), + user_pin: ctx.user_pin.take(), + new_admin_pin: ctx.new_admin_pin.take(), + new_user_pin: ctx.new_user_pin.take(), + password: ctx.password.take(), + no_cache: ctx.no_cache, + verbosity, + }; + command.execute(&mut ctx, subargs) + } +} diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..537a2cf --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,984 @@ +// commands.rs + +// ************************************************************************* +// * Copyright (C) 2018-2020 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use std::fmt; +use std::result; +use std::thread; +use std::time; +use std::u8; + +use libc::sync; + +use nitrokey::ConfigureOtp; +use nitrokey::Device; +use nitrokey::GenerateOtp; +use nitrokey::GetPasswordSafe; + +use crate::args; +use crate::error; +use crate::error::Error; +use crate::pinentry; +use crate::Result; + +/// Create an `error::Error` with an error message of the format `msg: err`. +fn get_error(msg: &'static str, err: nitrokey::Error) -> Error { + Error::NitrokeyError(Some(msg), err) +} + +/// Set `libnitrokey`'s log level based on the execution context's verbosity. +fn set_log_level(ctx: &mut args::ExecCtx<'_>) { + let log_lvl = match ctx.verbosity { + // The error log level is what libnitrokey uses by default. As such, + // there is no harm in us setting that as well when the user did not + // ask for higher verbosity. + 0 => nitrokey::LogLevel::Error, + 1 => nitrokey::LogLevel::Warning, + 2 => nitrokey::LogLevel::Info, + 3 => nitrokey::LogLevel::DebugL1, + 4 => nitrokey::LogLevel::Debug, + _ => nitrokey::LogLevel::DebugL2, + }; + nitrokey::set_log_level(log_lvl); +} + +/// Connect to any Nitrokey device and do something with it. +fn with_device(ctx: &mut args::ExecCtx<'_>, op: F) -> Result<()> +where + F: FnOnce(&mut args::ExecCtx<'_>, nitrokey::DeviceWrapper<'_>) -> Result<()>, +{ + let mut manager = nitrokey::take()?; + set_log_level(ctx); + + let device = match ctx.model { + Some(model) => manager.connect_model(model.into()).map_err(|_| { + let error = format!("Nitrokey {} device not found", model.as_user_facing_str()); + Error::Error(error) + })?, + None => manager + .connect() + .map_err(|_| Error::from("Nitrokey device not found"))?, + }; + + op(ctx, device) +} + +/// Connect to a Nitrokey Storage device and do something with it. +fn with_storage_device(ctx: &mut args::ExecCtx<'_>, op: F) -> Result<()> +where + F: FnOnce(&mut args::ExecCtx<'_>, nitrokey::Storage<'_>) -> Result<()>, +{ + let mut manager = nitrokey::take()?; + set_log_level(ctx); + + if let Some(model) = ctx.model { + if model != args::DeviceModel::Storage { + return Err(Error::from( + "This command is only available on the Nitrokey Storage", + )); + } + } + + let device = manager + .connect_storage() + .map_err(|_| Error::from("Nitrokey Storage device not found"))?; + op(ctx, device) +} + +/// Connect to any Nitrokey device, retrieve a password safe handle, and +/// do something with it. +fn with_password_safe(ctx: &mut args::ExecCtx<'_>, mut op: F) -> Result<()> +where + F: FnMut(&mut args::ExecCtx<'_>, nitrokey::PasswordSafe<'_, '_>) -> Result<()>, +{ + with_device(ctx, |ctx, mut device| { + let pin_entry = pinentry::PinEntry::from(pinentry::PinType::User, &device)?; + try_with_pin_and_data( + ctx, + &pin_entry, + "Could not access the password safe", + (), + move |ctx, _, pin| { + let pws = device + .get_password_safe(pin) + .map_err(|err| ((), Error::from(err)))?; + + op(ctx, pws).map_err(|err| ((), err)) + }, + ) + })?; + Ok(()) +} + +/// Authenticate the given device using the given PIN type and operation. +/// +/// If an error occurs, the error message `msg` is used. +fn authenticate<'mgr, D, A, F>( + ctx: &mut args::ExecCtx<'_>, + device: D, + pin_type: pinentry::PinType, + msg: &'static str, + op: F, +) -> Result +where + D: Device<'mgr>, + F: FnMut(&mut args::ExecCtx<'_>, D, &str) -> result::Result, +{ + let pin_entry = pinentry::PinEntry::from(pin_type, &device)?; + + try_with_pin_and_data(ctx, &pin_entry, msg, device, op) +} + +/// Authenticate the given device with the user PIN. +fn authenticate_user<'mgr, T>( + ctx: &mut args::ExecCtx<'_>, + device: T, +) -> Result> +where + T: Device<'mgr>, +{ + authenticate( + ctx, + device, + pinentry::PinType::User, + "Could not authenticate as user", + |_ctx, device, pin| device.authenticate_user(pin), + ) +} + +/// Authenticate the given device with the admin PIN. +fn authenticate_admin<'mgr, T>( + ctx: &mut args::ExecCtx<'_>, + device: T, +) -> Result> +where + T: Device<'mgr>, +{ + authenticate( + ctx, + device, + pinentry::PinType::Admin, + "Could not authenticate as admin", + |_ctx, device, pin| device.authenticate_admin(pin), + ) +} + +/// Return a string representation of the given volume status. +fn get_volume_status(status: &nitrokey::VolumeStatus) -> &'static str { + if status.active { + if status.read_only { + "read-only" + } else { + "active" + } + } else { + "inactive" + } +} + +/// Try to execute the given function with a pin queried using pinentry. +/// +/// This function will query the pin of the given type from the user +/// using pinentry. It will then execute the given function. If this +/// function returns a result, the result will be passed on. If it +/// returns a `CommandError::WrongPassword`, the user will be asked +/// again to enter the pin. Otherwise, this function returns an error +/// containing the given error message. The user will have at most +/// three tries to get the pin right. +/// +/// The data argument can be used to pass on data between the tries. At +/// the first try, this function will call `op` with `data`. At the +/// second or third try, it will call `op` with the data returned by the +/// previous call to `op`. +fn try_with_pin_and_data_with_pinentry( + ctx: &mut args::ExecCtx<'_>, + pin_entry: &pinentry::PinEntry, + msg: &'static str, + data: D, + mut op: F, +) -> Result +where + F: FnMut(&mut args::ExecCtx<'_>, D, &str) -> result::Result, + E: error::TryInto, +{ + let mut data = data; + let mut retry = 3; + let mut error_msg = None; + loop { + let pin = pinentry::inquire(ctx, pin_entry, pinentry::Mode::Query, error_msg)?; + match op(ctx, data, &pin) { + Ok(result) => return Ok(result), + Err((new_data, err)) => match err.try_into() { + Ok(err) => match err { + nitrokey::Error::CommandError(nitrokey::CommandError::WrongPassword) => { + pinentry::clear(pin_entry)?; + retry -= 1; + + if retry > 0 { + error_msg = Some("Wrong password, please reenter"); + data = new_data; + continue; + } + return Err(get_error(msg, err)); + } + err => return Err(get_error(msg, err)), + }, + Err(err) => return Err(err), + }, + }; + } +} + +/// Try to execute the given function with a PIN. +fn try_with_pin_and_data( + ctx: &mut args::ExecCtx<'_>, + pin_entry: &pinentry::PinEntry, + msg: &'static str, + data: D, + mut op: F, +) -> Result +where + F: FnMut(&mut args::ExecCtx<'_>, D, &str) -> result::Result, + E: Into + error::TryInto, +{ + let pin = match pin_entry.pin_type() { + // Ideally we would not clone here, but that would require us to + // restrict op to work with an immutable ExecCtx, which is not + // possible given that some clients print data. + pinentry::PinType::Admin => ctx.admin_pin.clone(), + pinentry::PinType::User => ctx.user_pin.clone(), + }; + + if let Some(pin) = pin { + let pin = pin.to_str().ok_or_else(|| { + Error::Error(format!( + "{}: Failed to read PIN due to invalid Unicode data", + msg + )) + })?; + op(ctx, data, &pin).map_err(|(_, err)| err.into()) + } else { + try_with_pin_and_data_with_pinentry(ctx, pin_entry, msg, data, op) + } +} + +/// Try to execute the given function with a pin queried using pinentry. +/// +/// This function behaves exactly as `try_with_pin_and_data`, but +/// it refrains from passing any data to it. +fn try_with_pin( + ctx: &mut args::ExecCtx<'_>, + pin_entry: &pinentry::PinEntry, + msg: &'static str, + mut op: F, +) -> Result<()> +where + F: FnMut(&str) -> result::Result<(), E>, + E: Into + error::TryInto, +{ + try_with_pin_and_data(ctx, pin_entry, msg, (), |_ctx, data, pin| { + op(pin).map_err(|err| (data, err)) + }) +} + +/// Pretty print the status of a Nitrokey Storage. +fn print_storage_status( + ctx: &mut args::ExecCtx<'_>, + status: &nitrokey::StorageStatus, +) -> Result<()> { + println!( + ctx, + r#" Storage: + SD card ID: {id:#x} + firmware: {fw} + storage keys: {sk} + volumes: + unencrypted: {vu} + encrypted: {ve} + hidden: {vh}"#, + id = status.serial_number_sd_card, + fw = if status.firmware_locked { + "locked" + } else { + "unlocked" + }, + sk = if status.stick_initialized { + "created" + } else { + "not created" + }, + vu = get_volume_status(&status.unencrypted_volume), + ve = get_volume_status(&status.encrypted_volume), + vh = get_volume_status(&status.hidden_volume), + )?; + Ok(()) +} + +/// Query and pretty print the status that is common to all Nitrokey devices. +fn print_status( + ctx: &mut args::ExecCtx<'_>, + model: &'static str, + device: &nitrokey::DeviceWrapper<'_>, +) -> Result<()> { + let serial_number = device + .get_serial_number() + .map_err(|err| get_error("Could not query the serial number", err))?; + + println!( + ctx, + r#"Status: + model: {model} + serial number: 0x{id} + firmware version: {fwv} + user retry count: {urc} + admin retry count: {arc}"#, + model = model, + id = serial_number, + fwv = device.get_firmware_version()?, + urc = device.get_user_retry_count()?, + arc = device.get_admin_retry_count()?, + )?; + + if let nitrokey::DeviceWrapper::Storage(device) = device { + let status = device + .get_status() + .map_err(|err| get_error("Getting Storage status failed", err))?; + + print_storage_status(ctx, &status) + } else { + Ok(()) + } +} + +/// Inquire the status of the nitrokey. +pub fn status(ctx: &mut args::ExecCtx<'_>) -> Result<()> { + with_device(ctx, |ctx, device| { + let model = match device { + nitrokey::DeviceWrapper::Pro(_) => "Pro", + nitrokey::DeviceWrapper::Storage(_) => "Storage", + }; + print_status(ctx, model, &device) + }) +} + +/// Perform a factory reset. +pub fn reset(ctx: &mut args::ExecCtx<'_>) -> Result<()> { + with_device(ctx, |ctx, mut device| { + let pin_entry = pinentry::PinEntry::from(pinentry::PinType::Admin, &device)?; + + // To force the user to enter the admin PIN before performing a + // factory reset, we clear the pinentry cache for the admin PIN. + pinentry::clear(&pin_entry)?; + + try_with_pin(ctx, &pin_entry, "Factory reset failed", |pin| { + device.factory_reset(&pin)?; + // Work around for a timing issue between factory_reset and + // build_aes_key, see + // https://github.com/Nitrokey/nitrokey-storage-firmware/issues/80 + thread::sleep(time::Duration::from_secs(3)); + // Another work around for spurious WrongPassword returns of + // build_aes_key after a factory reset on Pro devices. + // https://github.com/Nitrokey/nitrokey-pro-firmware/issues/57 + let _ = device.get_user_retry_count(); + device.build_aes_key(nitrokey::DEFAULT_ADMIN_PIN) + }) + }) +} + +/// Change the configuration of the unencrypted volume. +pub fn unencrypted_set( + ctx: &mut args::ExecCtx<'_>, + mode: args::UnencryptedVolumeMode, +) -> Result<()> { + with_storage_device(ctx, |ctx, mut device| { + let pin_entry = pinentry::PinEntry::from(pinentry::PinType::Admin, &device)?; + let mode = match mode { + args::UnencryptedVolumeMode::ReadWrite => nitrokey::VolumeMode::ReadWrite, + args::UnencryptedVolumeMode::ReadOnly => nitrokey::VolumeMode::ReadOnly, + }; + + // The unencrypted volume may reconnect, so be sure to flush caches to + // disk. + unsafe { sync() }; + + try_with_pin( + ctx, + &pin_entry, + "Changing unencrypted volume mode failed", + |pin| device.set_unencrypted_volume_mode(&pin, mode), + ) + }) +} + +/// Open the encrypted volume on the Nitrokey. +pub fn encrypted_open(ctx: &mut args::ExecCtx<'_>) -> Result<()> { + with_storage_device(ctx, |ctx, mut device| { + let pin_entry = pinentry::PinEntry::from(pinentry::PinType::User, &device)?; + + // We may forcefully close a hidden volume, if active, so be sure to + // flush caches to disk. + unsafe { sync() }; + + try_with_pin(ctx, &pin_entry, "Opening encrypted volume failed", |pin| { + device.enable_encrypted_volume(&pin) + }) + }) +} + +/// Close the previously opened encrypted volume. +pub fn encrypted_close(ctx: &mut args::ExecCtx<'_>) -> Result<()> { + with_storage_device(ctx, |_ctx, mut device| { + // Flush all filesystem caches to disk. We are mostly interested in + // making sure that the encrypted volume on the Nitrokey we are + // about to close is not closed while not all data was written to + // it. + unsafe { sync() }; + + device + .disable_encrypted_volume() + .map_err(|err| get_error("Closing encrypted volume failed", err)) + }) +} + +/// Create a hidden volume. +pub fn hidden_create(ctx: &mut args::ExecCtx<'_>, slot: u8, start: u8, end: u8) -> Result<()> { + with_storage_device(ctx, |ctx, mut device| { + let pwd_entry = pinentry::PwdEntry::from(&device)?; + let pwd = if let Some(pwd) = &ctx.password { + pwd + .to_str() + .ok_or_else(|| Error::from("Failed to read password: invalid Unicode data found")) + .map(ToOwned::to_owned) + } else { + pinentry::choose(ctx, &pwd_entry) + }?; + + device + .create_hidden_volume(slot, start, end, &pwd) + .map_err(|err| get_error("Creating hidden volume failed", err)) + }) +} + +/// Open a hidden volume. +pub fn hidden_open(ctx: &mut args::ExecCtx<'_>) -> Result<()> { + with_storage_device(ctx, |ctx, mut device| { + let pwd_entry = pinentry::PwdEntry::from(&device)?; + let pwd = if let Some(pwd) = &ctx.password { + pwd + .to_str() + .ok_or_else(|| Error::from("Failed to read password: invalid Unicode data found")) + .map(ToOwned::to_owned) + } else { + pinentry::inquire(ctx, &pwd_entry, pinentry::Mode::Query, None) + }?; + + // We may forcefully close an encrypted volume, if active, so be sure + // to flush caches to disk. + unsafe { sync() }; + + device + .enable_hidden_volume(&pwd) + .map_err(|err| get_error("Opening hidden volume failed", err)) + }) +} + +/// Close a previously opened hidden volume. +pub fn hidden_close(ctx: &mut args::ExecCtx<'_>) -> Result<()> { + with_storage_device(ctx, |_ctx, mut device| { + unsafe { sync() }; + + device + .disable_hidden_volume() + .map_err(|err| get_error("Closing hidden volume failed", err)) + }) +} + +/// Return a String representation of the given Option. +fn format_option(option: Option) -> String { + match option { + Some(value) => format!("{}", value), + None => "not set".to_string(), + } +} + +/// Read the Nitrokey configuration. +pub fn config_get(ctx: &mut args::ExecCtx<'_>) -> Result<()> { + with_device(ctx, |ctx, device| { + let config = device + .get_config() + .map_err(|err| get_error("Could not get configuration", err))?; + println!( + ctx, + r#"Config: + numlock binding: {nl} + capslock binding: {cl} + scrollock binding: {sl} + require user PIN for OTP: {otp}"#, + nl = format_option(config.numlock), + cl = format_option(config.capslock), + sl = format_option(config.scrollock), + otp = config.user_password, + )?; + Ok(()) + }) +} + +/// Write the Nitrokey configuration. +pub fn config_set( + ctx: &mut args::ExecCtx<'_>, + numlock: args::ConfigOption, + capslock: args::ConfigOption, + scrollock: args::ConfigOption, + user_password: Option, +) -> Result<()> { + with_device(ctx, |ctx, device| { + let mut device = authenticate_admin(ctx, device)?; + let config = device + .get_config() + .map_err(|err| get_error("Could not get configuration", err))?; + let config = nitrokey::Config { + numlock: numlock.or(config.numlock), + capslock: capslock.or(config.capslock), + scrollock: scrollock.or(config.scrollock), + user_password: user_password.unwrap_or(config.user_password), + }; + device + .write_config(config) + .map_err(|err| get_error("Could not set configuration", err)) + }) +} + +/// Lock the Nitrokey device. +pub fn lock(ctx: &mut args::ExecCtx<'_>) -> Result<()> { + with_device(ctx, |_ctx, mut device| { + device + .lock() + .map_err(|err| get_error("Could not lock the device", err)) + }) +} + +fn get_otp(slot: u8, algorithm: args::OtpAlgorithm, device: &mut T) -> Result +where + T: GenerateOtp, +{ + match algorithm { + args::OtpAlgorithm::Hotp => device.get_hotp_code(slot), + args::OtpAlgorithm::Totp => device.get_totp_code(slot), + } + .map_err(|err| get_error("Could not generate OTP", err)) +} + +fn get_unix_timestamp() -> Result { + time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .map_err(|_| Error::from("Current system time is before the Unix epoch")) + .map(|duration| duration.as_secs()) +} + +/// Generate a one-time password on the Nitrokey device. +pub fn otp_get( + ctx: &mut args::ExecCtx<'_>, + slot: u8, + algorithm: args::OtpAlgorithm, + time: Option, +) -> Result<()> { + with_device(ctx, |ctx, mut device| { + if algorithm == args::OtpAlgorithm::Totp { + device + .set_time( + match time { + Some(time) => time, + None => get_unix_timestamp()?, + }, + true, + ) + .map_err(|err| get_error("Could not set time", err))?; + } + let config = device + .get_config() + .map_err(|err| get_error("Could not get device configuration", err))?; + let otp = if config.user_password { + let mut user = authenticate_user(ctx, device)?; + get_otp(slot, algorithm, &mut user) + } else { + get_otp(slot, algorithm, &mut device) + }?; + println!(ctx, "{}", otp)?; + Ok(()) + }) +} + +/// Format a byte vector as a hex string. +fn format_bytes(bytes: &[u8]) -> String { + bytes + .iter() + .map(|c| format!("{:02x}", c)) + .collect::>() + .join("") +} + +/// Prepare an ASCII secret string for libnitrokey. +/// +/// libnitrokey expects secrets as hexadecimal strings. This function transforms an ASCII string +/// into a hexadecimal string or returns an error if the given string contains non-ASCII +/// characters. +fn prepare_ascii_secret(secret: &str) -> Result { + if secret.is_ascii() { + Ok(format_bytes(&secret.as_bytes())) + } else { + Err(Error::from( + "The given secret is not an ASCII string despite --format ascii being set", + )) + } +} + +/// Prepare a base32 secret string for libnitrokey. +fn prepare_base32_secret(secret: &str) -> Result { + base32::decode(base32::Alphabet::RFC4648 { padding: false }, secret) + .map(|vec| format_bytes(&vec)) + .ok_or_else(|| Error::from("Could not parse base32 secret")) +} + +/// Configure a one-time password slot on the Nitrokey device. +pub fn otp_set( + ctx: &mut args::ExecCtx<'_>, + mut data: nitrokey::OtpSlotData, + algorithm: args::OtpAlgorithm, + counter: u64, + time_window: u16, + secret_format: args::OtpSecretFormat, +) -> Result<()> { + with_device(ctx, |ctx, device| { + let secret = match secret_format { + args::OtpSecretFormat::Ascii => prepare_ascii_secret(&data.secret)?, + args::OtpSecretFormat::Base32 => prepare_base32_secret(&data.secret)?, + args::OtpSecretFormat::Hex => { + // We need to ensure to provide a string with an even number of + // characters in it, just because that's what libnitrokey + // expects. So prepend a '0' if that is not the case. + // TODO: This code can be removed once upstream issue #164 + // (https://github.com/Nitrokey/libnitrokey/issues/164) is + // addressed. + if data.secret.len() % 2 != 0 { + data.secret.insert(0, '0') + } + data.secret + } + }; + let data = nitrokey::OtpSlotData { secret, ..data }; + let mut device = authenticate_admin(ctx, device)?; + match algorithm { + args::OtpAlgorithm::Hotp => device.write_hotp_slot(data, counter), + args::OtpAlgorithm::Totp => device.write_totp_slot(data, time_window), + } + .map_err(|err| get_error("Could not write OTP slot", err))?; + Ok(()) + }) +} + +/// Clear an OTP slot. +pub fn otp_clear( + ctx: &mut args::ExecCtx<'_>, + slot: u8, + algorithm: args::OtpAlgorithm, +) -> Result<()> { + with_device(ctx, |ctx, device| { + let mut device = authenticate_admin(ctx, device)?; + match algorithm { + args::OtpAlgorithm::Hotp => device.erase_hotp_slot(slot), + args::OtpAlgorithm::Totp => device.erase_totp_slot(slot), + } + .map_err(|err| get_error("Could not clear OTP slot", err))?; + Ok(()) + }) +} + +fn print_otp_status( + ctx: &mut args::ExecCtx<'_>, + algorithm: args::OtpAlgorithm, + device: &nitrokey::DeviceWrapper<'_>, + all: bool, +) -> Result<()> { + let mut slot: u8 = 0; + loop { + let result = match algorithm { + args::OtpAlgorithm::Hotp => device.get_hotp_slot_name(slot), + args::OtpAlgorithm::Totp => device.get_totp_slot_name(slot), + }; + slot = match slot.checked_add(1) { + Some(slot) => slot, + None => { + return Err(Error::from("Integer overflow when iterating OTP slots")); + } + }; + let name = match result { + Ok(name) => name, + Err(nitrokey::Error::LibraryError(nitrokey::LibraryError::InvalidSlot)) => return Ok(()), + Err(nitrokey::Error::CommandError(nitrokey::CommandError::SlotNotProgrammed)) => { + if all { + "[not programmed]".to_string() + } else { + continue; + } + } + Err(err) => return Err(get_error("Could not check OTP slot", err)), + }; + println!(ctx, "{}\t{}\t{}", algorithm, slot - 1, name)?; + } +} + +/// Print the status of the OTP slots. +pub fn otp_status(ctx: &mut args::ExecCtx<'_>, all: bool) -> Result<()> { + with_device(ctx, |ctx, device| { + println!(ctx, "alg\tslot\tname")?; + print_otp_status(ctx, args::OtpAlgorithm::Hotp, &device, all)?; + print_otp_status(ctx, args::OtpAlgorithm::Totp, &device, all)?; + Ok(()) + }) +} + +/// Clear the PIN stored by various operations. +pub fn pin_clear(ctx: &mut args::ExecCtx<'_>) -> Result<()> { + with_device(ctx, |_ctx, device| { + pinentry::clear(&pinentry::PinEntry::from( + pinentry::PinType::Admin, + &device, + )?)?; + pinentry::clear(&pinentry::PinEntry::from(pinentry::PinType::User, &device)?)?; + Ok(()) + }) +} + +/// Choose a PIN of the given type. +/// +/// If the user has set the respective environment variable for the +/// given PIN type, it will be used. +fn choose_pin( + ctx: &mut args::ExecCtx<'_>, + pin_entry: &pinentry::PinEntry, + new: bool, +) -> Result { + let new_pin = match pin_entry.pin_type() { + pinentry::PinType::Admin => { + if new { + &ctx.new_admin_pin + } else { + &ctx.admin_pin + } + } + pinentry::PinType::User => { + if new { + &ctx.new_user_pin + } else { + &ctx.user_pin + } + } + }; + + if let Some(new_pin) = new_pin { + new_pin + .to_str() + .ok_or_else(|| Error::from("Failed to read PIN: invalid Unicode data found")) + .map(ToOwned::to_owned) + } else { + pinentry::choose(ctx, pin_entry) + } +} + +/// Change a PIN. +pub fn pin_set(ctx: &mut args::ExecCtx<'_>, pin_type: pinentry::PinType) -> Result<()> { + with_device(ctx, |ctx, mut device| { + let pin_entry = pinentry::PinEntry::from(pin_type, &device)?; + let new_pin = choose_pin(ctx, &pin_entry, true)?; + + try_with_pin( + ctx, + &pin_entry, + "Could not change the PIN", + |current_pin| match pin_type { + pinentry::PinType::Admin => device.change_admin_pin(¤t_pin, &new_pin), + pinentry::PinType::User => device.change_user_pin(¤t_pin, &new_pin), + }, + )?; + + // We just changed the PIN but confirmed the action with the old PIN, + // which may have caused it to be cached. Since it no longer applies, + // make sure to evict the corresponding entry from the cache. + pinentry::clear(&pin_entry) + }) +} + +/// Unblock and reset the user PIN. +pub fn pin_unblock(ctx: &mut args::ExecCtx<'_>) -> Result<()> { + with_device(ctx, |ctx, mut device| { + let pin_entry = pinentry::PinEntry::from(pinentry::PinType::User, &device)?; + let user_pin = choose_pin(ctx, &pin_entry, false)?; + let pin_entry = pinentry::PinEntry::from(pinentry::PinType::Admin, &device)?; + + try_with_pin( + ctx, + &pin_entry, + "Could not unblock the user PIN", + |admin_pin| device.unlock_user_pin(&admin_pin, &user_pin), + ) + }) +} + +fn print_pws_data( + ctx: &mut args::ExecCtx<'_>, + description: &'static str, + result: result::Result, + quiet: bool, +) -> Result<()> { + let value = result.map_err(|err| get_error("Could not access PWS slot", err))?; + if quiet { + println!(ctx, "{}", value)?; + } else { + println!(ctx, "{} {}", description, value)?; + } + Ok(()) +} + +fn check_slot(pws: &nitrokey::PasswordSafe<'_, '_>, slot: u8) -> Result<()> { + if slot >= nitrokey::SLOT_COUNT { + return Err(nitrokey::Error::from(nitrokey::LibraryError::InvalidSlot).into()); + } + let status = pws + .get_slot_status() + .map_err(|err| get_error("Could not read PWS slot status", err))?; + if status[slot as usize] { + Ok(()) + } else { + Err(get_error( + "Could not access PWS slot", + nitrokey::CommandError::SlotNotProgrammed.into(), + )) + } +} + +/// Read a PWS slot. +pub fn pws_get( + ctx: &mut args::ExecCtx<'_>, + slot: u8, + show_name: bool, + show_login: bool, + show_password: bool, + quiet: bool, +) -> Result<()> { + with_password_safe(ctx, |ctx, pws| { + check_slot(&pws, slot)?; + + let show_all = !show_name && !show_login && !show_password; + if show_all || show_name { + print_pws_data(ctx, "name: ", pws.get_slot_name(slot), quiet)?; + } + if show_all || show_login { + print_pws_data(ctx, "login: ", pws.get_slot_login(slot), quiet)?; + } + if show_all || show_password { + print_pws_data(ctx, "password:", pws.get_slot_password(slot), quiet)?; + } + Ok(()) + }) +} + +/// Write a PWS slot. +pub fn pws_set( + ctx: &mut args::ExecCtx<'_>, + slot: u8, + name: &str, + login: &str, + password: &str, +) -> Result<()> { + with_password_safe(ctx, |_ctx, mut pws| { + pws + .write_slot(slot, name, login, password) + .map_err(|err| get_error("Could not write PWS slot", err)) + }) +} + +/// Clear a PWS slot. +pub fn pws_clear(ctx: &mut args::ExecCtx<'_>, slot: u8) -> Result<()> { + with_password_safe(ctx, |_ctx, mut pws| { + pws + .erase_slot(slot) + .map_err(|err| get_error("Could not clear PWS slot", err)) + }) +} + +fn print_pws_slot( + ctx: &mut args::ExecCtx<'_>, + pws: &nitrokey::PasswordSafe<'_, '_>, + slot: usize, + programmed: bool, +) -> Result<()> { + if slot > u8::MAX as usize { + return Err(Error::from("Invalid PWS slot number")); + } + let slot = slot as u8; + let name = if programmed { + pws + .get_slot_name(slot) + .map_err(|err| get_error("Could not read PWS slot", err))? + } else { + "[not programmed]".to_string() + }; + println!(ctx, "{}\t{}", slot, name)?; + Ok(()) +} + +/// Print the status of all PWS slots. +pub fn pws_status(ctx: &mut args::ExecCtx<'_>, all: bool) -> Result<()> { + with_password_safe(ctx, |ctx, pws| { + let slots = pws + .get_slot_status() + .map_err(|err| get_error("Could not read PWS slot status", err))?; + println!(ctx, "slot\tname")?; + for (i, &value) in slots.iter().enumerate().filter(|(_, &value)| all || value) { + print_pws_slot(ctx, &pws, i, value)?; + } + Ok(()) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn prepare_secret_ascii() { + let result = prepare_ascii_secret("12345678901234567890"); + assert_eq!( + "3132333435363738393031323334353637383930".to_string(), + result.unwrap() + ); + } + + #[test] + fn prepare_secret_non_ascii() { + let result = prepare_ascii_secret("Österreich"); + assert!(result.is_err()); + } + + #[test] + fn hex_string() { + assert_eq!(format_bytes(&[b' ']), "20"); + assert_eq!(format_bytes(&[b' ', b' ']), "2020"); + assert_eq!(format_bytes(&[b'\n', b'\n']), "0a0a"); + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..819bed8 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,104 @@ +// error.rs + +// ************************************************************************* +// * Copyright (C) 2017-2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use std::fmt; +use std::io; +use std::str; +use std::string; + +/// A trait used to simplify error handling in conjunction with the +/// try_with_* functions we use for repeatedly asking the user for a +/// secret. +pub trait TryInto { + fn try_into(self) -> Result; +} + +impl TryInto for T +where + T: Into, +{ + fn try_into(self) -> Result { + Ok(self.into()) + } +} + +#[derive(Debug)] +pub enum Error { + ArgparseError(i32), + IoError(io::Error), + NitrokeyError(Option<&'static str>, nitrokey::Error), + Utf8Error(str::Utf8Error), + Error(String), +} + +impl TryInto for Error { + fn try_into(self) -> Result { + match self { + Error::NitrokeyError(_, err) => Ok(err), + err => Err(err), + } + } +} + +impl From<&str> for Error { + fn from(s: &str) -> Error { + Error::Error(s.to_string()) + } +} + +impl From for Error { + fn from(e: nitrokey::Error) -> Error { + Error::NitrokeyError(None, e) + } +} + +impl From for Error { + fn from(e: io::Error) -> Error { + Error::IoError(e) + } +} + +impl From for Error { + fn from(e: str::Utf8Error) -> Error { + Error::Utf8Error(e) + } +} + +impl From for Error { + fn from(e: string::FromUtf8Error) -> Error { + Error::Utf8Error(e.utf8_error()) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Error::ArgparseError(_) => write!(f, "Could not parse arguments"), + Error::NitrokeyError(ref ctx, ref e) => { + if let Some(ctx) = ctx { + write!(f, "{}: ", ctx)?; + } + write!(f, "{}", e) + } + Error::Utf8Error(_) => write!(f, "Encountered UTF-8 conversion error"), + Error::IoError(ref e) => write!(f, "IO error: {}", e), + Error::Error(ref e) => write!(f, "{}", e), + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c639f14 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,167 @@ +// main.rs + +// ************************************************************************* +// * Copyright (C) 2017-2020 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +#![warn( + bad_style, + dead_code, + future_incompatible, + illegal_floating_point_literal_pattern, + improper_ctypes, + intra_doc_link_resolution_failure, + late_bound_lifetime_arguments, + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + no_mangle_generic_items, + non_shorthand_field_patterns, + nonstandard_style, + overflowing_literals, + path_statements, + patterns_in_fns_without_body, + plugin_as_library, + private_in_public, + proc_macro_derive_resolution_fallback, + renamed_and_removed_lints, + rust_2018_compatibility, + rust_2018_idioms, + safe_packed_borrows, + stable_features, + trivial_bounds, + trivial_numeric_casts, + type_alias_bounds, + tyvar_behind_raw_pointer, + unconditional_recursion, + unreachable_code, + unreachable_patterns, + unstable_features, + unstable_name_collisions, + unused, + unused_comparisons, + unused_import_braces, + unused_lifetimes, + unused_qualifications, + unused_results, + where_clauses_object_safety, + while_true +)] + +//! Nitrocli is a program providing a command line interface to certain +//! commands of Nitrokey Pro and Storage devices. + +#[macro_use] +mod redefine; +#[macro_use] +mod arg_util; + +mod args; +mod commands; +mod error; +mod pinentry; +#[cfg(test)] +mod tests; + +use std::env; +use std::ffi; +use std::io; +use std::process; +use std::result; + +use crate::error::Error; + +type Result = result::Result; + +const NITROCLI: &str = "nitrocli"; +const NITROCLI_ADMIN_PIN: &str = "NITROCLI_ADMIN_PIN"; +const NITROCLI_USER_PIN: &str = "NITROCLI_USER_PIN"; +const NITROCLI_NEW_ADMIN_PIN: &str = "NITROCLI_NEW_ADMIN_PIN"; +const NITROCLI_NEW_USER_PIN: &str = "NITROCLI_NEW_USER_PIN"; +const NITROCLI_PASSWORD: &str = "NITROCLI_PASSWORD"; +const NITROCLI_NO_CACHE: &str = "NITROCLI_NO_CACHE"; + +/// The context used when running the program. +pub(crate) struct RunCtx<'io> { + /// The `Write` object used as standard output throughout the program. + pub stdout: &'io mut dyn io::Write, + /// The `Write` object used as standard error throughout the program. + pub stderr: &'io mut dyn io::Write, + /// The admin PIN, if provided through an environment variable. + pub admin_pin: Option, + /// The user PIN, if provided through an environment variable. + pub user_pin: Option, + /// The new admin PIN to set, if provided through an environment variable. + /// + /// This variable is only used by commands that change the admin PIN. + pub new_admin_pin: Option, + /// The new user PIN, if provided through an environment variable. + /// + /// This variable is only used by commands that change the user PIN. + pub new_user_pin: Option, + /// A password used by some commands, if provided through an environment variable. + pub password: Option, + /// Whether to bypass the cache for all secrets or not. + pub no_cache: bool, +} + +fn run<'ctx, 'io: 'ctx>(ctx: &'ctx mut RunCtx<'io>, args: Vec) -> i32 { + match args::handle_arguments(ctx, args) { + Ok(()) => 0, + Err(err) => match err { + Error::ArgparseError(err) => match err { + // argparse printed the help message + 0 => 0, + // argparse printed an error message + _ => 1, + }, + _ => { + let _ = eprintln!(ctx, "{}", err); + 1 + } + }, + } +} + +fn main() { + use std::io::Write; + + let mut stdout = io::stdout(); + let mut stderr = io::stderr(); + let args = env::args().collect::>(); + let ctx = &mut RunCtx { + stdout: &mut stdout, + stderr: &mut stderr, + admin_pin: env::var_os(NITROCLI_ADMIN_PIN), + user_pin: env::var_os(NITROCLI_USER_PIN), + new_admin_pin: env::var_os(NITROCLI_NEW_ADMIN_PIN), + new_user_pin: env::var_os(NITROCLI_NEW_USER_PIN), + password: env::var_os(NITROCLI_PASSWORD), + no_cache: env::var_os(NITROCLI_NO_CACHE).is_some(), + }; + + let rc = run(ctx, args); + // We exit the process the hard way below. The problem is that because + // of this, buffered IO may not be flushed. So make sure to explicitly + // flush before exiting. Note that stderr is unbuffered, alleviating + // the need for any flushing there. + // Ideally we would just make `main` return an i32 and let Rust deal + // with all of this, but the `process::Termination` functionality is + // still unstable and we have no way to convince the caller to "just + // exit" without printing additional information. + let _ = stdout.flush(); + process::exit(rc); +} diff --git a/src/pinentry.rs b/src/pinentry.rs new file mode 100644 index 0000000..fd47657 --- /dev/null +++ b/src/pinentry.rs @@ -0,0 +1,404 @@ +// pinentry.rs + +// ************************************************************************* +// * Copyright (C) 2017-2020 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use std::borrow; +use std::fmt; +use std::io; +use std::process; +use std::str; + +use crate::args; +use crate::error::Error; + +type CowStr = borrow::Cow<'static, str>; + +/// PIN type requested from pinentry. +/// +/// The available PIN types correspond to the PIN types used by the Nitrokey devices: user and +/// admin. +#[allow(unused_doc_comments)] +Enum! {PinType, [ + Admin => "admin", + User => "user", +]} + +/// A trait representing a secret to be entered by the user. +pub trait SecretEntry: fmt::Debug { + /// The cache ID to use for this secret. + fn cache_id(&self) -> Option; + /// The prompt to display when asking for the secret. + fn prompt(&self) -> CowStr; + /// The description to display when asking for the secret. + fn description(&self, mode: Mode) -> CowStr; + /// The minimum number of characters the secret needs to have. + fn min_len(&self) -> u8; +} + +#[derive(Debug)] +pub struct PinEntry { + pin_type: PinType, + model: nitrokey::Model, + serial: String, +} + +impl PinEntry { + pub fn from<'mgr, D>(pin_type: PinType, device: &D) -> crate::Result + where + D: nitrokey::Device<'mgr>, + { + let model = device.get_model(); + let serial = device.get_serial_number()?; + Ok(Self { + pin_type, + model, + serial, + }) + } + + pub fn pin_type(&self) -> PinType { + self.pin_type + } +} + +impl SecretEntry for PinEntry { + fn cache_id(&self) -> Option { + let model = self.model.to_string().to_lowercase(); + let suffix = format!("{}:{}", model, self.serial); + let cache_id = match self.pin_type { + PinType::Admin => format!("nitrocli:admin:{}", suffix), + PinType::User => format!("nitrocli:user:{}", suffix), + }; + Some(cache_id.into()) + } + + fn prompt(&self) -> CowStr { + match self.pin_type { + PinType::Admin => "Admin PIN", + PinType::User => "User PIN", + } + .into() + } + + fn description(&self, mode: Mode) -> CowStr { + format!( + "{} for\rNitrokey {} {}", + match self.pin_type { + PinType::Admin => match mode { + Mode::Choose => "Please enter a new admin PIN", + Mode::Confirm => "Please confirm the new admin PIN", + Mode::Query => "Please enter the admin PIN", + }, + PinType::User => match mode { + Mode::Choose => "Please enter a new user PIN", + Mode::Confirm => "Please confirm the new user PIN", + Mode::Query => "Please enter the user PIN", + }, + }, + self.model, + self.serial, + ) + .into() + } + + fn min_len(&self) -> u8 { + match self.pin_type { + PinType::Admin => 8, + PinType::User => 6, + } + } +} + +#[derive(Debug)] +pub struct PwdEntry { + model: nitrokey::Model, + serial: String, +} + +impl PwdEntry { + pub fn from<'mgr, D>(device: &D) -> crate::Result + where + D: nitrokey::Device<'mgr>, + { + let model = device.get_model(); + let serial = device.get_serial_number()?; + Ok(Self { model, serial }) + } +} + +impl SecretEntry for PwdEntry { + fn cache_id(&self) -> Option { + None + } + + fn prompt(&self) -> CowStr { + "Password".into() + } + + fn description(&self, mode: Mode) -> CowStr { + format!( + "{} for\rNitrokey {} {}", + match mode { + Mode::Choose => "Please enter a new hidden volume password", + Mode::Confirm => "Please confirm the new hidden volume password", + Mode::Query => "Please enter a hidden volume password", + }, + self.model, + self.serial, + ) + .into() + } + + fn min_len(&self) -> u8 { + // More or less arbitrary minimum length based on the fact that the + // manual mentions six letter passwords in examples. Users + // *probably* should go longer than that, but we don't want to be + // too opinionated. + 6 + } +} + +/// Secret entry mode for pinentry. +/// +/// This enum describes the context of the pinentry query, for example +/// prompting for the current secret or requesting a new one. The mode +/// may affect the pinentry description and whether a quality bar is +/// shown. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Mode { + /// Let the user choose a new secret. + Choose, + /// Let the user confirm the previously chosen secret. + Confirm, + /// Query an existing secret. + Query, +} + +impl Mode { + fn show_quality_bar(self) -> bool { + self == Mode::Choose + } +} + +fn parse_pinentry_pin(response: R) -> crate::Result +where + R: AsRef, +{ + let string = response.as_ref(); + let lines: Vec<&str> = string.lines().collect(); + + // We expect the response to be of the form: + // > D passphrase + // > OK + // or potentially: + // > ERR 83886179 Operation cancelled + if lines.len() == 2 && lines[1] == "OK" && lines[0].starts_with("D ") { + // We got the only valid answer we accept. + let (_, pass) = lines[0].split_at(2); + return Ok(pass.to_string()); + } + + // Check if we are dealing with a special "ERR " line and report that + // specially. + if !lines.is_empty() && lines[0].starts_with("ERR ") { + let (_, error) = lines[0].split_at(4); + return Err(Error::from(error)); + } + Err(Error::Error(format!("Unexpected response: {}", string))) +} + +/// Inquire a secret from the user. +/// +/// This function inquires a secret from the user or returns a cached +/// entry, if available (and if caching is not disabled for the given +/// execution context). If an error message is set, it is displayed in +/// the entry dialog. The mode describes the context of the pinentry +/// dialog. It is used to choose an appropriate description and to +/// decide whether a quality bar is shown in the dialog. +pub fn inquire( + ctx: &mut args::ExecCtx<'_>, + entry: &E, + mode: Mode, + error_msg: Option<&str>, +) -> crate::Result +where + E: SecretEntry, +{ + let cache_id = entry + .cache_id() + .and_then(|id| if ctx.no_cache { None } else { Some(id) }) + // "X" is a sentinel value indicating that no caching is desired. + .unwrap_or_else(|| "X".into()) + .into(); + + let error_msg = error_msg + .map(|msg| msg.replace(" ", "+")) + .unwrap_or_else(|| String::from("+")); + let prompt = entry.prompt().replace(" ", "+"); + let description = entry.description(mode).replace(" ", "+"); + + let args = vec![cache_id, error_msg, prompt, description].join(" "); + let mut command = "GET_PASSPHRASE --data ".to_string(); + if mode.show_quality_bar() { + command += "--qualitybar "; + } + command += &args; + // An error reported for the GET_PASSPHRASE command does not actually + // cause gpg-connect-agent to exit with a non-zero error code, we have + // to evaluate the output to determine success/failure. + let output = process::Command::new("gpg-connect-agent") + .arg(command) + .arg("/bye") + .output() + .map_err(|err| match err.kind() { + io::ErrorKind::NotFound => { + io::Error::new(io::ErrorKind::NotFound, "gpg-connect-agent not found") + } + _ => err, + })?; + parse_pinentry_pin(str::from_utf8(&output.stdout)?) +} + +fn check(entry: &E, secret: &str) -> crate::Result<()> +where + E: SecretEntry, +{ + if secret.len() < usize::from(entry.min_len()) { + Err(Error::Error(format!( + "The secret must be at least {} characters long", + entry.min_len() + ))) + } else { + Ok(()) + } +} + +pub fn choose(ctx: &mut args::ExecCtx<'_>, entry: &E) -> crate::Result +where + E: SecretEntry, +{ + clear(entry)?; + let chosen = inquire(ctx, entry, Mode::Choose, None)?; + clear(entry)?; + check(entry, &chosen)?; + + let confirmed = inquire(ctx, entry, Mode::Confirm, None)?; + clear(entry)?; + + if chosen != confirmed { + Err(Error::from("Entered secrets do not match")) + } else { + Ok(chosen) + } +} + +fn parse_pinentry_response(response: R) -> crate::Result<()> +where + R: AsRef, +{ + let string = response.as_ref(); + let lines = string.lines().collect::>(); + + if lines.len() == 1 && lines[0] == "OK" { + // We got the only valid answer we accept. + return Ok(()); + } + Err(Error::Error(format!("Unexpected response: {}", string))) +} + +/// Clear the cached secret represented by the given entry. +pub fn clear(entry: &E) -> crate::Result<()> +where + E: SecretEntry, +{ + if let Some(cache_id) = entry.cache_id() { + let command = format!("CLEAR_PASSPHRASE {}", cache_id); + let output = process::Command::new("gpg-connect-agent") + .arg(command) + .arg("/bye") + .output()?; + + parse_pinentry_response(str::from_utf8(&output.stdout)?) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_pinentry_pin_good() { + let response = "D passphrase\nOK\n"; + let expected = "passphrase"; + + assert_eq!(parse_pinentry_pin(response).unwrap(), expected) + } + + #[test] + fn parse_pinentry_pin_error() { + let error = "83886179 Operation cancelled"; + let response = "ERR ".to_string() + error + "\n"; + let expected = error; + + let error = parse_pinentry_pin(response); + + if let Error::Error(ref e) = error.err().unwrap() { + assert_eq!(e, &expected); + } else { + panic!("Unexpected result"); + } + } + + #[test] + fn parse_pinentry_pin_unexpected() { + let response = "foobar\n"; + let expected = format!("Unexpected response: {}", response); + let error = parse_pinentry_pin(response); + + if let Error::Error(ref e) = error.err().unwrap() { + assert_eq!(e, &expected); + } else { + panic!("Unexpected result"); + } + } + + #[test] + fn parse_pinentry_response_ok() { + assert!(parse_pinentry_response("OK\n").is_ok()) + } + + #[test] + fn parse_pinentry_response_ok_no_newline() { + assert!(parse_pinentry_response("OK").is_ok()) + } + + #[test] + fn parse_pinentry_response_unexpected() { + let response = "ERR 42"; + let expected = format!("Unexpected response: {}", response); + let error = parse_pinentry_response(response); + + if let Error::Error(ref e) = error.err().unwrap() { + assert_eq!(e, &expected); + } else { + panic!("Unexpected result"); + } + } +} diff --git a/src/redefine.rs b/src/redefine.rs new file mode 100644 index 0000000..a79cb4b --- /dev/null +++ b/src/redefine.rs @@ -0,0 +1,38 @@ +// redefine.rs + +// ************************************************************************* +// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +// A replacement of the standard println!() macro that requires an +// execution context as the first argument and prints to its stdout. +macro_rules! println { + ($ctx:expr) => { + writeln!($ctx.stdout, "") + }; + ($ctx:expr, $($arg:tt)*) => { + writeln!($ctx.stdout, $($arg)*) + }; +} + +macro_rules! eprintln { + ($ctx:expr) => { + writeln!($ctx.stderr, "") + }; + ($ctx:expr, $($arg:tt)*) => { + writeln!($ctx.stderr, $($arg)*) + }; +} diff --git a/src/tests/config.rs b/src/tests/config.rs new file mode 100644 index 0000000..ea3a0e8 --- /dev/null +++ b/src/tests/config.rs @@ -0,0 +1,66 @@ +// config.rs + +// ************************************************************************* +// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use super::*; + +#[test_device] +fn get(model: nitrokey::Model) -> crate::Result<()> { + let re = regex::Regex::new( + r#"^Config: + numlock binding: (not set|\d+) + capslock binding: (not set|\d+) + scrollock binding: (not set|\d+) + require user PIN for OTP: (true|false) +$"#, + ) + .unwrap(); + + let out = Nitrocli::with_model(model).handle(&["config", "get"])?; + assert!(re.is_match(&out), out); + Ok(()) +} + +#[test_device] +fn set_wrong_usage(model: nitrokey::Model) { + let res = Nitrocli::with_model(model).handle(&["config", "set", "--numlock", "2", "-N"]); + assert_eq!( + res.unwrap_str_err(), + "--numlock and --no-numlock are mutually exclusive" + ); +} + +#[test_device] +fn set_get(model: nitrokey::Model) -> crate::Result<()> { + let mut ncli = Nitrocli::with_model(model); + let _ = ncli.handle(&["config", "set", "-s", "1", "-c", "0", "-N"])?; + + let re = regex::Regex::new( + r#"^Config: + numlock binding: not set + capslock binding: 0 + scrollock binding: 1 + require user PIN for OTP: (true|false) +$"#, + ) + .unwrap(); + + let out = ncli.handle(&["config", "get"])?; + assert!(re.is_match(&out), out); + Ok(()) +} diff --git a/src/tests/encrypted.rs b/src/tests/encrypted.rs new file mode 100644 index 0000000..75b84c3 --- /dev/null +++ b/src/tests/encrypted.rs @@ -0,0 +1,95 @@ +// encrypted.rs + +// ************************************************************************* +// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use super::*; + +#[test_device(storage)] +fn status_open_close(model: nitrokey::Model) -> crate::Result<()> { + fn make_re(open: Option) -> regex::Regex { + let encrypted = match open { + Some(open) => { + if open { + "active" + } else { + "(read-only|inactive)" + } + } + None => "(read-only|active|inactive)", + }; + let re = format!( + r#" + volumes: + unencrypted: (read-only|active|inactive) + encrypted: {} + hidden: (read-only|active|inactive) +$"#, + encrypted + ); + regex::Regex::new(&re).unwrap() + } + + let mut ncli = Nitrocli::with_model(model); + let out = ncli.handle(&["status"])?; + assert!(make_re(None).is_match(&out), out); + + let _ = ncli.handle(&["encrypted", "open"])?; + let out = ncli.handle(&["status"])?; + assert!(make_re(Some(true)).is_match(&out), out); + + let _ = ncli.handle(&["encrypted", "close"])?; + let out = ncli.handle(&["status"])?; + assert!(make_re(Some(false)).is_match(&out), out); + + Ok(()) +} + +#[test_device(pro)] +fn encrypted_open_on_pro(model: nitrokey::Model) { + let res = Nitrocli::with_model(model).handle(&["encrypted", "open"]); + assert_eq!( + res.unwrap_str_err(), + "This command is only available on the Nitrokey Storage", + ); +} + +#[test_device(storage)] +fn encrypted_open_close(model: nitrokey::Model) -> crate::Result<()> { + let mut ncli = Nitrocli::with_model(model); + let out = ncli.handle(&["encrypted", "open"])?; + assert!(out.is_empty()); + + { + let mut manager = nitrokey::force_take()?; + let device = manager.connect_storage()?; + assert!(device.get_status()?.encrypted_volume.active); + assert!(!device.get_status()?.hidden_volume.active); + } + + let out = ncli.handle(&["encrypted", "close"])?; + assert!(out.is_empty()); + + { + let mut manager = nitrokey::force_take()?; + let device = manager.connect_storage()?; + assert!(!device.get_status()?.encrypted_volume.active); + assert!(!device.get_status()?.hidden_volume.active); + } + + Ok(()) +} diff --git a/src/tests/hidden.rs b/src/tests/hidden.rs new file mode 100644 index 0000000..28a5d23 --- /dev/null +++ b/src/tests/hidden.rs @@ -0,0 +1,49 @@ +// hidden.rs + +// ************************************************************************* +// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use super::*; + +#[test_device(storage)] +fn hidden_create_open_close(model: nitrokey::Model) -> crate::Result<()> { + let mut ncli = Nitrocli::with_model(model); + let out = ncli.handle(&["hidden", "create", "0", "50", "100"])?; + assert!(out.is_empty()); + + let out = ncli.handle(&["hidden", "open"])?; + assert!(out.is_empty()); + + { + let mut manager = nitrokey::force_take()?; + let device = manager.connect_storage()?; + assert!(!device.get_status()?.encrypted_volume.active); + assert!(device.get_status()?.hidden_volume.active); + } + + let out = ncli.handle(&["hidden", "close"])?; + assert!(out.is_empty()); + + { + let mut manager = nitrokey::force_take()?; + let device = manager.connect_storage()?; + assert!(!device.get_status()?.encrypted_volume.active); + assert!(!device.get_status()?.hidden_volume.active); + } + + Ok(()) +} diff --git a/src/tests/lock.rs b/src/tests/lock.rs new file mode 100644 index 0000000..5140152 --- /dev/null +++ b/src/tests/lock.rs @@ -0,0 +1,44 @@ +// lock.rs + +// ************************************************************************* +// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use super::*; + +#[test_device(pro)] +fn lock_pro(model: nitrokey::Model) -> crate::Result<()> { + // We can't really test much more here than just success of the command. + let out = Nitrocli::with_model(model).handle(&["lock"])?; + assert!(out.is_empty()); + + Ok(()) +} + +#[test_device(storage)] +fn lock_storage(model: nitrokey::Model) -> crate::Result<()> { + let mut ncli = Nitrocli::with_model(model); + let _ = ncli.handle(&["encrypted", "open"])?; + + let out = ncli.handle(&["lock"])?; + assert!(out.is_empty()); + + let mut manager = nitrokey::force_take()?; + let device = manager.connect_storage()?; + assert!(!device.get_status()?.encrypted_volume.active); + + Ok(()) +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..5ebf285 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,180 @@ +// mod.rs + +// ************************************************************************* +// * Copyright (C) 2019-2020 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use std::ffi; +use std::fmt; + +use nitrokey_test::test as test_device; + +mod config; +mod encrypted; +mod hidden; +mod lock; +mod otp; +mod pin; +mod pws; +mod reset; +mod run; +mod status; +mod unencrypted; + +/// A trait simplifying checking for expected errors. +pub trait UnwrapError { + /// Unwrap an Error::Error variant. + fn unwrap_str_err(self) -> String; + /// Unwrap a Error::CommandError variant. + fn unwrap_cmd_err(self) -> (Option<&'static str>, nitrokey::CommandError); + /// Unwrap a Error::LibraryError variant. + fn unwrap_lib_err(self) -> (Option<&'static str>, nitrokey::LibraryError); +} + +impl UnwrapError for crate::Result +where + T: fmt::Debug, +{ + fn unwrap_str_err(self) -> String { + match self.unwrap_err() { + crate::Error::Error(err) => err, + err => panic!("Unexpected error variant found: {:?}", err), + } + } + + fn unwrap_cmd_err(self) -> (Option<&'static str>, nitrokey::CommandError) { + match self.unwrap_err() { + crate::Error::NitrokeyError(ctx, err) => match err { + nitrokey::Error::CommandError(err) => (ctx, err), + err => panic!("Unexpected error variant found: {:?}", err), + }, + err => panic!("Unexpected error variant found: {:?}", err), + } + } + + fn unwrap_lib_err(self) -> (Option<&'static str>, nitrokey::LibraryError) { + match self.unwrap_err() { + crate::Error::NitrokeyError(ctx, err) => match err { + nitrokey::Error::LibraryError(err) => (ctx, err), + err => panic!("Unexpected error variant found: {:?}", err), + }, + err => panic!("Unexpected error variant found: {:?}", err), + } + } +} + +struct Nitrocli { + model: Option, + admin_pin: Option, + user_pin: Option, + new_admin_pin: Option, + new_user_pin: Option, + password: Option, +} + +impl Nitrocli { + pub fn new() -> Self { + Self { + model: None, + admin_pin: Some(nitrokey::DEFAULT_ADMIN_PIN.into()), + user_pin: Some(nitrokey::DEFAULT_USER_PIN.into()), + new_admin_pin: None, + new_user_pin: None, + password: None, + } + } + + pub fn with_model(model: M) -> Self + where + M: Into, + { + Self { + model: Some(model.into()), + admin_pin: Some(nitrokey::DEFAULT_ADMIN_PIN.into()), + user_pin: Some(nitrokey::DEFAULT_USER_PIN.into()), + new_admin_pin: None, + new_user_pin: None, + password: Some("1234567".into()), + } + } + + pub fn admin_pin(&mut self, pin: impl Into) { + self.admin_pin = Some(pin.into()) + } + + pub fn new_admin_pin(&mut self, pin: impl Into) { + self.new_admin_pin = Some(pin.into()) + } + + pub fn user_pin(&mut self, pin: impl Into) { + self.user_pin = Some(pin.into()) + } + + pub fn new_user_pin(&mut self, pin: impl Into) { + self.new_user_pin = Some(pin.into()) + } + + fn model_to_arg(model: nitrokey::Model) -> &'static str { + match model { + nitrokey::Model::Pro => "--model=pro", + nitrokey::Model::Storage => "--model=storage", + } + } + + fn do_run(&mut self, args: &[&str], f: F) -> (R, Vec, Vec) + where + F: FnOnce(&mut crate::RunCtx<'_>, Vec) -> R, + { + let args = ["nitrocli"] + .iter() + .cloned() + .chain(self.model.map(Self::model_to_arg)) + .chain(args.iter().cloned()) + .map(ToOwned::to_owned) + .collect(); + + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + + let ctx = &mut crate::RunCtx { + stdout: &mut stdout, + stderr: &mut stderr, + admin_pin: self.admin_pin.clone(), + user_pin: self.user_pin.clone(), + new_admin_pin: self.new_admin_pin.clone(), + new_user_pin: self.new_user_pin.clone(), + password: self.password.clone(), + no_cache: true, + }; + + (f(ctx, args), stdout, stderr) + } + + /// Run `nitrocli`'s `run` function. + pub fn run(&mut self, args: &[&str]) -> (i32, Vec, Vec) { + self.do_run(args, |c, a| crate::run(c, a)) + } + + /// Run `nitrocli`'s `handle_arguments` function. + pub fn handle(&mut self, args: &[&str]) -> crate::Result { + let (res, out, _) = self.do_run(args, |c, a| crate::args::handle_arguments(c, a)); + res.map(|_| String::from_utf8_lossy(&out).into_owned()) + } + + pub fn model(&self) -> Option { + self.model + } +} diff --git a/src/tests/otp.rs b/src/tests/otp.rs new file mode 100644 index 0000000..0ccecf9 --- /dev/null +++ b/src/tests/otp.rs @@ -0,0 +1,130 @@ +// otp.rs + +// ************************************************************************* +// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use super::*; + +use crate::args; + +#[test_device] +fn set_invalid_slot_raw(model: nitrokey::Model) { + let (rc, out, err) = Nitrocli::with_model(model).run(&["otp", "set", "100", "name", "1234"]); + + assert_ne!(rc, 0); + assert_eq!(out, b""); + assert_eq!(&err[..24], b"Could not write OTP slot"); +} + +#[test_device] +fn set_invalid_slot(model: nitrokey::Model) { + let res = Nitrocli::with_model(model).handle(&["otp", "set", "100", "name", "1234"]); + + assert_eq!( + res.unwrap_lib_err(), + ( + Some("Could not write OTP slot"), + nitrokey::LibraryError::InvalidSlot + ) + ); +} + +#[test_device] +fn status(model: nitrokey::Model) -> crate::Result<()> { + let re = regex::Regex::new( + r#"^alg\tslot\tname +((totp|hotp)\t\d+\t.+\n)+$"#, + ) + .unwrap(); + + let mut ncli = Nitrocli::with_model(model); + // Make sure that we have at least something to display by ensuring + // that there is one slot programmed. + let _ = ncli.handle(&["otp", "set", "0", "the-name", "123456"])?; + + let out = ncli.handle(&["otp", "status"])?; + assert!(re.is_match(&out), out); + Ok(()) +} + +#[test_device] +fn set_get_hotp(model: nitrokey::Model) -> crate::Result<()> { + // Secret and expected HOTP values as per RFC 4226: Appendix D -- HOTP + // Algorithm: Test Values. + const SECRET: &str = "12345678901234567890"; + const OTP1: &str = concat!(755224, "\n"); + const OTP2: &str = concat!(287082, "\n"); + + let mut ncli = Nitrocli::with_model(model); + let _ = ncli.handle(&[ + "otp", "set", "-a", "hotp", "-f", "ascii", "1", "name", &SECRET, + ])?; + + let out = ncli.handle(&["otp", "get", "-a", "hotp", "1"])?; + assert_eq!(out, OTP1); + + let out = ncli.handle(&["otp", "get", "-a", "hotp", "1"])?; + assert_eq!(out, OTP2); + Ok(()) +} + +#[test_device] +fn set_get_totp(model: nitrokey::Model) -> crate::Result<()> { + // Secret and expected TOTP values as per RFC 6238: Appendix B -- + // Test Vectors. + const SECRET: &str = "12345678901234567890"; + const TIME: &str = stringify!(1111111111); + const OTP: &str = concat!(14050471, "\n"); + + let mut ncli = Nitrocli::with_model(model); + let _ = ncli.handle(&["otp", "set", "-d", "8", "-f", "ascii", "2", "name", &SECRET])?; + + let out = ncli.handle(&["otp", "get", "-t", TIME, "2"])?; + assert_eq!(out, OTP); + Ok(()) +} + +#[test_device] +fn set_totp_uneven_chars(model: nitrokey::Model) -> crate::Result<()> { + let secrets = [ + (args::OtpSecretFormat::Hex, "123"), + (args::OtpSecretFormat::Base32, "FBILDWWGA2"), + ]; + + for (format, secret) in &secrets { + let mut ncli = Nitrocli::with_model(model); + let _ = ncli.handle(&["otp", "set", "-f", format.as_ref(), "3", "foobar", &secret])?; + } + Ok(()) +} + +#[test_device] +fn clear(model: nitrokey::Model) -> crate::Result<()> { + let mut ncli = Nitrocli::with_model(model); + let _ = ncli.handle(&["otp", "set", "3", "hotp-test", "abcdef"])?; + let _ = ncli.handle(&["otp", "clear", "3"])?; + let res = ncli.handle(&["otp", "get", "3"]); + + assert_eq!( + res.unwrap_cmd_err(), + ( + Some("Could not generate OTP"), + nitrokey::CommandError::SlotNotProgrammed + ) + ); + Ok(()) +} diff --git a/src/tests/pin.rs b/src/tests/pin.rs new file mode 100644 index 0000000..958a36d --- /dev/null +++ b/src/tests/pin.rs @@ -0,0 +1,84 @@ +// pin.rs + +// ************************************************************************* +// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use nitrokey::Authenticate; +use nitrokey::Device; + +use super::*; + +#[test_device] +fn unblock(model: nitrokey::Model) -> crate::Result<()> { + { + let mut manager = nitrokey::force_take()?; + let device = manager.connect_model(model)?; + let (device, err) = device.authenticate_user("wrong-pin").unwrap_err(); + match err { + nitrokey::Error::CommandError(err) if err == nitrokey::CommandError::WrongPassword => (), + _ => panic!("Unexpected error variant found: {:?}", err), + } + assert!(device.get_user_retry_count()? < 3); + } + + let _ = Nitrocli::with_model(model).handle(&["pin", "unblock"])?; + + { + let mut manager = nitrokey::force_take()?; + let device = manager.connect_model(model)?; + assert_eq!(device.get_user_retry_count()?, 3); + } + Ok(()) +} + +#[test_device] +fn set_user(model: nitrokey::Model) -> crate::Result<()> { + let mut ncli = Nitrocli::with_model(model); + // Set a new user PIN. + ncli.new_user_pin("new-pin"); + let out = ncli.handle(&["pin", "set", "user"])?; + assert!(out.is_empty()); + + { + let mut manager = nitrokey::force_take()?; + let device = manager.connect_model(model)?; + let (_, err) = device + .authenticate_user(nitrokey::DEFAULT_USER_PIN) + .unwrap_err(); + + match err { + nitrokey::Error::CommandError(err) if err == nitrokey::CommandError::WrongPassword => (), + _ => panic!("Unexpected error variant found: {:?}", err), + } + } + + // Revert to the default user PIN. + ncli.user_pin("new-pin"); + ncli.new_user_pin(nitrokey::DEFAULT_USER_PIN); + + let out = ncli.handle(&["pin", "set", "user"])?; + assert!(out.is_empty()); + + { + let mut manager = nitrokey::force_take()?; + let device = manager.connect_model(ncli.model().unwrap())?; + let _ = device + .authenticate_user(nitrokey::DEFAULT_USER_PIN) + .unwrap(); + } + Ok(()) +} diff --git a/src/tests/pws.rs b/src/tests/pws.rs new file mode 100644 index 0000000..651b2d5 --- /dev/null +++ b/src/tests/pws.rs @@ -0,0 +1,123 @@ +// pws.rs + +// ************************************************************************* +// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use super::*; + +#[test_device] +fn set_invalid_slot(model: nitrokey::Model) { + let res = Nitrocli::with_model(model).handle(&["pws", "set", "100", "name", "login", "1234"]); + + assert_eq!( + res.unwrap_lib_err(), + ( + Some("Could not write PWS slot"), + nitrokey::LibraryError::InvalidSlot + ) + ); +} + +#[test_device] +fn status(model: nitrokey::Model) -> crate::Result<()> { + let re = regex::Regex::new( + r#"^slot\tname +(\d+\t.+\n)+$"#, + ) + .unwrap(); + + let mut ncli = Nitrocli::with_model(model); + // Make sure that we have at least something to display by ensuring + // that there are there is one slot programmed. + let _ = ncli.handle(&["pws", "set", "0", "the-name", "the-login", "123456"])?; + + let out = ncli.handle(&["pws", "status"])?; + assert!(re.is_match(&out), out); + Ok(()) +} + +#[test_device] +fn set_get(model: nitrokey::Model) -> crate::Result<()> { + const NAME: &str = "dropbox"; + const LOGIN: &str = "d-e-s-o"; + const PASSWORD: &str = "my-secret-password"; + + let mut ncli = Nitrocli::with_model(model); + let _ = ncli.handle(&["pws", "set", "1", &NAME, &LOGIN, &PASSWORD])?; + + let out = ncli.handle(&["pws", "get", "1", "--quiet", "--name"])?; + assert_eq!(out, format!("{}\n", NAME)); + + let out = ncli.handle(&["pws", "get", "1", "--quiet", "--login"])?; + assert_eq!(out, format!("{}\n", LOGIN)); + + let out = ncli.handle(&["pws", "get", "1", "--quiet", "--password"])?; + assert_eq!(out, format!("{}\n", PASSWORD)); + + let out = ncli.handle(&["pws", "get", "1", "--quiet"])?; + assert_eq!(out, format!("{}\n{}\n{}\n", NAME, LOGIN, PASSWORD)); + + let out = ncli.handle(&["pws", "get", "1"])?; + assert_eq!( + out, + format!( + "name: {}\nlogin: {}\npassword: {}\n", + NAME, LOGIN, PASSWORD + ), + ); + Ok(()) +} + +#[test_device] +fn set_reset_get(model: nitrokey::Model) -> crate::Result<()> { + const NAME: &str = "some/svc"; + const LOGIN: &str = "a\\user"; + const PASSWORD: &str = "!@&-)*(&+%^@"; + + let mut ncli = Nitrocli::with_model(model); + let _ = ncli.handle(&["pws", "set", "2", &NAME, &LOGIN, &PASSWORD])?; + + let out = ncli.handle(&["reset"])?; + assert_eq!(out, ""); + + let res = ncli.handle(&["pws", "get", "2"]); + assert_eq!( + res.unwrap_cmd_err(), + ( + Some("Could not access PWS slot"), + nitrokey::CommandError::SlotNotProgrammed + ) + ); + Ok(()) +} + +#[test_device] +fn clear(model: nitrokey::Model) -> crate::Result<()> { + let mut ncli = Nitrocli::with_model(model); + let _ = ncli.handle(&["pws", "set", "10", "clear-test", "some-login", "abcdef"])?; + let _ = ncli.handle(&["pws", "clear", "10"])?; + let res = ncli.handle(&["pws", "get", "10"]); + + assert_eq!( + res.unwrap_cmd_err(), + ( + Some("Could not access PWS slot"), + nitrokey::CommandError::SlotNotProgrammed + ) + ); + Ok(()) +} diff --git a/src/tests/reset.rs b/src/tests/reset.rs new file mode 100644 index 0000000..e197970 --- /dev/null +++ b/src/tests/reset.rs @@ -0,0 +1,60 @@ +// reset.rs + +// ************************************************************************* +// * Copyright (C) 2019 Robin Krahl (robin.krahl@ireas.org) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use nitrokey::Authenticate; +use nitrokey::GetPasswordSafe; + +use super::*; + +#[test_device] +fn reset(model: nitrokey::Model) -> crate::Result<()> { + let new_admin_pin = "87654321"; + let mut ncli = Nitrocli::with_model(model); + + // Change the admin PIN. + ncli.new_admin_pin(new_admin_pin); + let _ = ncli.handle(&["pin", "set", "admin"])?; + + { + let mut manager = nitrokey::force_take()?; + // Check that the admin PIN has been changed. + let device = manager.connect_model(ncli.model().unwrap())?; + let _ = device.authenticate_admin(new_admin_pin).unwrap(); + } + + // Perform factory reset + ncli.admin_pin(new_admin_pin); + let out = ncli.handle(&["reset"])?; + assert!(out.is_empty()); + + { + let mut manager = nitrokey::force_take()?; + // Check that the admin PIN has been reset. + let device = manager.connect_model(ncli.model().unwrap())?; + let mut device = device + .authenticate_admin(nitrokey::DEFAULT_ADMIN_PIN) + .unwrap(); + + // Check that the password store works, i.e., the AES key has been + // built. + let _ = device.get_password_safe(nitrokey::DEFAULT_USER_PIN)?; + } + + Ok(()) +} diff --git a/src/tests/run.rs b/src/tests/run.rs new file mode 100644 index 0000000..c59c660 --- /dev/null +++ b/src/tests/run.rs @@ -0,0 +1,103 @@ +// run.rs + +// ************************************************************************* +// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use super::*; + +#[test] +fn no_command_or_option() { + let (rc, out, err) = Nitrocli::new().run(&[]); + + assert_ne!(rc, 0); + assert_eq!(out, b""); + + let s = String::from_utf8_lossy(&err).into_owned(); + assert!(s.starts_with("Usage:\n"), s); +} + +#[test] +fn help_options() { + fn test_run(args: &[&str], help: &str) { + let mut all = args.to_vec(); + all.push(help); + + let (rc, out, err) = Nitrocli::new().run(&all); + + assert_eq!(rc, 0); + assert_eq!(err, b""); + + let s = String::from_utf8_lossy(&out).into_owned(); + let expected = format!("Usage:\n nitrocli {}", args.join(" ")); + assert!(s.starts_with(&expected), s); + } + + fn test(args: &[&str]) { + test_run(args, "--help"); + test_run(args, "-h"); + } + + test(&[]); + test(&["config"]); + test(&["config", "get"]); + test(&["config", "set"]); + test(&["encrypted"]); + test(&["encrypted", "open"]); + test(&["encrypted", "close"]); + test(&["hidden"]); + test(&["hidden", "close"]); + test(&["hidden", "create"]); + test(&["hidden", "open"]); + test(&["lock"]); + test(&["otp"]); + test(&["otp", "clear"]); + test(&["otp", "get"]); + test(&["otp", "set"]); + test(&["otp", "status"]); + test(&["pin"]); + test(&["pin", "clear"]); + test(&["pin", "set"]); + test(&["pin", "unblock"]); + test(&["pws"]); + test(&["pws", "clear"]); + test(&["pws", "get"]); + test(&["pws", "set"]); + test(&["pws", "status"]); + test(&["reset"]); + test(&["status"]); + test(&["unencrypted"]); + test(&["unencrypted", "set"]); +} + +#[test] +fn version_option() { + fn test(re: ®ex::Regex, opt: &'static str) { + let (rc, out, err) = Nitrocli::new().run(&[opt]); + + assert_eq!(rc, 0); + assert_eq!(err, b""); + + let s = String::from_utf8_lossy(&out).into_owned(); + let _ = re; + assert!(re.is_match(&s), out); + } + + let re = regex::Regex::new(r"^nitrocli \d+.\d+.\d+(-[^-]+)*\n$").unwrap(); + + test(&re, "--version"); + test(&re, "-V"); +} diff --git a/src/tests/status.rs b/src/tests/status.rs new file mode 100644 index 0000000..c9f4976 --- /dev/null +++ b/src/tests/status.rs @@ -0,0 +1,81 @@ +// status.rs + +// ************************************************************************* +// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use super::*; + +// This test acts as verification that conversion of Error::Error +// variants into the proper exit code works properly. +#[test_device] +fn not_found_raw() { + let (rc, out, err) = Nitrocli::new().run(&["status"]); + + assert_ne!(rc, 0); + assert_eq!(out, b""); + assert_eq!(err, b"Nitrokey device not found\n"); +} + +#[test_device] +fn not_found() { + let res = Nitrocli::new().handle(&["status"]); + assert_eq!(res.unwrap_str_err(), "Nitrokey device not found"); +} + +#[test_device(pro)] +fn output_pro(model: nitrokey::Model) -> crate::Result<()> { + let re = regex::Regex::new( + r#"^Status: + model: Pro + serial number: 0x[[:xdigit:]]{8} + firmware version: v\d+\.\d+ + user retry count: [0-3] + admin retry count: [0-3] +$"#, + ) + .unwrap(); + + let out = Nitrocli::with_model(model).handle(&["status"])?; + assert!(re.is_match(&out), out); + Ok(()) +} + +#[test_device(storage)] +fn output_storage(model: nitrokey::Model) -> crate::Result<()> { + let re = regex::Regex::new( + r#"^Status: + model: Storage + serial number: 0x[[:xdigit:]]{8} + firmware version: v\d+\.\d+ + user retry count: [0-3] + admin retry count: [0-3] + Storage: + SD card ID: 0x[[:xdigit:]]{8} + firmware: (un)?locked + storage keys: (not )?created + volumes: + unencrypted: (read-only|active|inactive) + encrypted: (read-only|active|inactive) + hidden: (read-only|active|inactive) +$"#, + ) + .unwrap(); + + let out = Nitrocli::with_model(model).handle(&["status"])?; + assert!(re.is_match(&out), out); + Ok(()) +} diff --git a/src/tests/unencrypted.rs b/src/tests/unencrypted.rs new file mode 100644 index 0000000..547dcaf --- /dev/null +++ b/src/tests/unencrypted.rs @@ -0,0 +1,46 @@ +// unencrypted.rs + +// ************************************************************************* +// * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use super::*; + +#[test_device(storage)] +fn unencrypted_set_read_write(model: nitrokey::Model) -> crate::Result<()> { + let mut ncli = Nitrocli::with_model(model); + let out = ncli.handle(&["unencrypted", "set", "read-write"])?; + assert!(out.is_empty()); + + { + let mut manager = nitrokey::force_take()?; + let device = manager.connect_storage()?; + assert!(device.get_status()?.unencrypted_volume.active); + assert!(!device.get_status()?.unencrypted_volume.read_only); + } + + let out = ncli.handle(&["unencrypted", "set", "read-only"])?; + assert!(out.is_empty()); + + { + let mut manager = nitrokey::force_take()?; + let device = manager.connect_storage()?; + assert!(device.get_status()?.unencrypted_volume.active); + assert!(device.get_status()?.unencrypted_volume.read_only); + } + + Ok(()) +} diff --git a/var/binary-size.py b/var/binary-size.py new file mode 100755 index 0000000..3653814 --- /dev/null +++ b/var/binary-size.py @@ -0,0 +1,134 @@ +#!/usr/bin/python3 -B + +#/*************************************************************************** +# * Copyright (C) 2019 Daniel Mueller (deso@posteo.net) * +# * * +# * This program is free software: you can redistribute it and/or modify * +# * it under the terms of the GNU General Public License as published by * +# * the Free Software Foundation, either version 3 of the License, or * +# * (at your option) any later version. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU General Public License for more details. * +# * * +# * You should have received a copy of the GNU General Public License * +# * along with this program. If not, see . * +# ***************************************************************************/ + +from argparse import ( + ArgumentParser, + ArgumentTypeError, +) +from concurrent.futures import ( + ThreadPoolExecutor, +) +from json import ( + loads as jsonLoad, +) +from os import ( + stat, +) +from os.path import ( + join, +) +from subprocess import ( + check_call, + check_output, +) +from sys import ( + argv, + exit, +) +from tempfile import ( + TemporaryDirectory, +) + +UNITS = { + "byte": 1, + "kib": 1024, + "mib": 1024 * 1024, +} + +def unit(string): + """Create a unit.""" + if string in UNITS: + return UNITS[string] + else: + raise ArgumentTypeError("Invalid unit: \"%s\"." % string) + + +def nitrocliPath(cwd): + """Determine the path to the nitrocli release build binary.""" + out = check_output(["cargo", "metadata", "--format-version=1"], cwd=cwd) + data = jsonLoad(out) + return join(data["target_directory"], "release", "nitrocli") + + +def fileSize(path): + """Determine the size of the file at the given path.""" + return stat(path).st_size + + +def repoRoot(): + """Retrieve the root directory of the current git repository.""" + out = check_output(["git", "rev-parse", "--show-toplevel"]) + return out.decode().strip() + + +def resolveCommit(commit): + """Resolve a commit into a SHA1 hash.""" + out = check_output(["git", "rev-parse", "--verify", "%s^{commit}" % commit]) + return out.decode().strip() + + +def determineSizeAt(root, rev): + """Determine the size of the nitrocli release build binary at the given git revision.""" + sha1 = resolveCommit(rev) + with TemporaryDirectory() as d: + cwd = join(d, "nitrocli") + check_call(["git", "clone", root, d]) + check_call(["git", "checkout", "--quiet", sha1], cwd=cwd) + check_call(["cargo", "build", "--quiet", "--release"], cwd=cwd) + + ncli = nitrocliPath(cwd) + check_call(["strip", ncli]) + return fileSize(ncli) + + +def setupArgumentParser(): + """Create and initialize an argument parser.""" + parser = ArgumentParser() + parser.add_argument( + "revs", metavar="REVS", nargs="+", + help="The revisions at which to measure the release binary size.", + ) + parser.add_argument( + "-u", "--unit", default="byte", dest="unit", metavar="UNIT", type=unit, + help="The unit in which to output the result (%s)." % "|".join(UNITS.keys()), + ) + return parser + + +def main(args): + """Determine the size of the nitrocli binary at given git revisions.""" + parser = setupArgumentParser() + ns = parser.parse_args(args) + root = repoRoot() + futures = [] + executor = ThreadPoolExecutor() + + for rev in ns.revs: + futures += [executor.submit(lambda r=rev: determineSizeAt(root, r))] + + executor.shutdown(wait=True) + + for future in futures: + print(int(round(future.result() / ns.unit, 0))) + + return 0 + + +if __name__ == "__main__": + exit(main(argv[1:])) -- cgit v1.2.1