aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobin Krahl <robin.krahl@ireas.org>2018-02-14 11:12:45 +0100
committerRobin Krahl <robin.krahl@ireas.org>2018-02-14 11:14:30 +0100
commit10fcd3a946a270fc6d111252b2de08dcd625a2b8 (patch)
tree32f874025493ee14563a02dc9c767b3f9e3739b8
downloadnkotp-master.tar.gz
nkotp-master.tar.bz2
Initial commit with support for OTP generationHEADmaster
-rw-r--r--.gitignore4
-rw-r--r--LICENSE21
-rw-r--r--Makefile32
-rw-r--r--README.md49
-rw-r--r--TODO.md12
-rw-r--r--config.mk18
-rw-r--r--nkotp.1.pod141
-rw-r--r--nkotp.c331
-rw-r--r--options.c436
-rw-r--r--options.h44
10 files changed, 1088 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5cdb1cb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/nkotp.1
+/nkotp.1.html
+/pod2htmd.tmp
+*.o
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1a3601d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 Robin Krahl <robin.krahl@ireas.org>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..2d5b6c8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,32 @@
+include config.mk
+
+VERSION := $(VERSION_MAJOR).$(VERSION_MINOR).$(VERSION_PATCH)
+
+CPPFLAGS += -DNKOTP_VERSION_MAJOR=$(VERSION_MAJOR) \
+ -DNKOTP_VERSION_MINOR=$(VERSION_MINOR) \
+ -DNKOTP_VERSION_PATCH=$(VERSION_PATCH) \
+ -DNKOTP_VERSION=\"$(VERSION)\"
+CPPFLAGS += $(CPPFLAGS_CONFUSE)
+CPPFLAGS += $(CPPFLAGS_NITROKEY)
+LDFLAGS += $(LDFLAGS_CONFUSE)
+LDFLAGS += $(LDFLAGS_NITROKEY)
+
+P2MFLAGS += --release=$(VERSION)
+
+OBJECTS := nkotp.o options.o
+TARGETS := nkotp nkotp.1 nkotp.1.html
+
+.PHONY: all clean
+
+all: $(TARGETS)
+
+clean:
+ rm -f $(OBJECTS) $(TARGETS) pod2htmd.tmp
+
+nkotp: $(OBJECTS)
+
+nkotp.1: nkotp.1.pod
+ pod2man $(P2MFLAGS) $^ > $@
+
+nkotp.1.html: nkotp.1.pod
+ pod2html $(P2HFLAGS) $^ > $@
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..83792d7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,49 @@
+# nkotp -- one-time password generator for Nitrokey devices on the command line
+
+`nkotp` provides access to the one-time password (OTP) generator on
+[Nitrokey][nk] devices on the command line. Both the Nitrokey Pro and the
+Nitrokey Storage support the generation of one-time passwords based on the
+[HOTP][hotp] and [TOTP][totp] algorithms. `nkotp` uses [`libnitrokey`][libnk]
+to configure the OTP slots and generate OTPs on a Nitrokey device.
+
+## Dependencies
+
+`nkotp` requires a POSIX-compliant operating system such as Linux or macOS.
+
+### Runtime dependencies
+
+- `libc` with `getopt_long` support
+- [`libconfuse`][libconfuse] v3.0.0 or later
+- [`libnitrokey`][libnk] v3.0 or later
+
+### Additional build dependencies
+
+- `gcc` or any other C99 compiler
+- GNU `make`
+- `pod2html`, `pod2man` (usually distributed with `perl`)
+
+## Compilation
+
+Build `nkotp` with `make`. You can configure the build in `config.mk` if you
+have non-standard library paths or compiler flags.
+
+## Usage
+
+For usage information, consult the man page [`nkotp(1)`][man].
+
+## Bugs and hacking
+
+If you encouter a bug or if you want to contribute to nkotp, please send an
+email to [nkotp-dev@ireas.org][nkotp-dev].
+
+## License
+
+This program is published under the terms of the MIT/X11 license (see LICENSE).
+
+[nk]: https://www.nitrokey.com/
+[hotp]: https://en.wikipedia.org/wiki/HMAC-based_One-time_Password_Algorithm
+[totp]: https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
+[libconfuse]: https://github.com/martinh/libconfuse
+[libnk]: https://github.com/Nitrokey/libnitrokey
+[man]: https://code.ireas.org/nkotp/doc/
+[nkotp-dev]: mailto:nkotp-dev@ireas.org
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..19d1eb7
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,12 @@
+- Decide what we want to do if passwords are too long: truncate, fail
+ silently, fail with error message (current solution). Affects
+ `read_user_password` and `read_password` in `nkotp.c`.
+- Consider moving some constants to `libnitrokey`, especially the maximum
+ password lengths and the number of HOTP and TOTP slots.
+- Find out the appropriate values to pass to `NK_get_totp_code` and
+ `NK_get_totp_code_PIN`. Affects `otp_generate` and `otp_generate_password`
+ in `nkotp.c`.
+- Use a better seed than the current time stamp for the generation of the
+ temporary password (`generate_tmp_password` in `nkotp.c`). Consider
+ switching to a better random number generator in the first place
+ (`getrandom` for Linux).
diff --git a/config.mk b/config.mk
new file mode 100644
index 0000000..549bac5
--- /dev/null
+++ b/config.mk
@@ -0,0 +1,18 @@
+# Version information
+VERSION_MAJOR := 0
+VERSION_MINOR := 0
+VERSION_PATCH := 1
+
+# Dependencies
+CPPFLAGS_CONFUSE :=
+CPPFLAGS_NITROKEY :=
+LDFLAGS_CONFUSE := -lconfuse
+LDFLAGS_NITROKEY := -lnitrokey
+
+# Compiler flags
+CFLAGS += -std=c99 -pedantic -Wall
+CPPFLAGS += -D_XOPEN_SOURCE=700 -D_GNU_SOURCE
+
+# Man page generation
+P2MFLAGS += --section=1 --center=nkotp --name=NKOTP
+P2HFLAGS += --noindex --title="nkotp(1)"
diff --git a/nkotp.1.pod b/nkotp.1.pod
new file mode 100644
index 0000000..875042e
--- /dev/null
+++ b/nkotp.1.pod
@@ -0,0 +1,141 @@
+=head1 NAME
+
+nkotp - one-time password generator for Nitrokey devices
+
+=head1 SYNOPSIS
+
+B<nkotp>
+S<[B<-a> I<algorithm>]>
+S<[B<-c> I<file>]>
+S<[B<-m> I<model>]>
+S<[B<-s> I<slot>]>
+S<B<-g> | B<-h> | B<-v>>
+
+=head1 DESCRIPTION
+
+B<nkotp> provides access to the one-time password (OTP) generator on Nitrokey
+devices. Currently, B<nkotp> only supports the generation of OTPs.
+
+If an action requires the user password, it is prompted from the standard
+input or read from the environment variable B<NKOTP_USER_PASSWORD> (if set).
+
+=head1 OPTIONS
+
+=head2 General options
+
+=over
+
+=item B<-a> I<algorithm>, B<--algorithm> I<algorithm>
+
+Set the algorithm to use for one-time password operations. I<algorithm> can be
+B<h> for HOTP and B<t> for TOTP (default).
+
+=item B<-c> I<file>, B<--config> I<file>
+
+Read the configuration from I<file>. See the B<FILES> section for the default
+configuration files.
+
+=item B<-m> I<model>, B<--model> I<model>
+
+Set the Nitrokey model to connect to. I<model> can be B<p> for a Nitrokey Pro,
+B<s> for a Nitrokey Storage and B<a> for automatic selection (default).
+
+=item B<-s> I<slot>, B<--slot> I<slot>
+
+Set the slot to use for one-time password operations. The available slots
+depend on the OTP algorithm (see B<--algorithm>). Currently, Nitrokey devices
+provide three HOTP and 15 TOTP slots. The slot numbering starts at one. The
+default value for this option is one.
+
+=back
+
+=head2 Modes of operation
+
+=over
+
+=item B<-g>, B<--generate>
+
+Generate a one-time password on the Nitrokey device and output it. The OTP
+algorithm is set with the B<--algorithm> option. The OTP slot on the Nitrokey
+device is set with the B<--slot> option.
+
+=item B<-h>, B<--help>
+
+Print a help message and exit.
+
+=item B<-v>, B<--version>
+
+Print version information and exit.
+
+=back
+
+=head1 CONFIGURATION
+
+B<nkotp> can read default values for the command-line options from a
+configuration file. See the B<FILES> section for more information on the
+possible locations for the configuration file.
+
+The configuration file may assign values to the following options:
+
+=over
+
+=item B<algorithm>
+
+=item B<device>
+
+=item B<slot>
+
+=back
+
+Each option corresponds to the command-line option with the same name. Values
+set in the configuration file take precedence over environment variables.
+
+The configuration file should contain one assignment per line. Assignments
+have the form C<option = value>. String values must be enclosed in quotes.
+Use the C<#> character for comments.
+
+A valid configuration file could have the following content:
+
+ # configuration example
+ algorithm = "t"
+ slot = 3
+
+=head1 ENVIRONMENT
+
+=over
+
+=item B<NKOTP_ALGORITHM>
+
+=item B<NKOTP_CONFIG>
+
+=item B<NKOTP_DEVICE>
+
+=item B<NKOTP_SLOT>
+
+If these environment variables are set, they override the default value for
+the corresponding command-line option. Values that are set in the
+configuration file take precedence over environment variables.
+
+=item B<NKOTP_USER_PASSWORD>
+
+If an action requires the user password, it is read from this environment
+variable (if set).
+
+=back
+
+=head1 FILES
+
+=over
+
+=item B<${XDG_CONFIG_HOME}/nkotp/config>
+
+User configuration file. If the environment variable B<XDG_CONFIG_HOME> is
+not set, B<${HOME}/.config> is used instead. A different configuration file
+can be set with the B<NKOTP_CONFIG> environment variable or the B<--config>
+option.
+
+=back
+
+=head1 AUTHOR
+
+Robin Krahl E<lt>robin.krahl@ireas.orgE<gt>
diff --git a/nkotp.c b/nkotp.c
new file mode 100644
index 0000000..2c8e251
--- /dev/null
+++ b/nkotp.c
@@ -0,0 +1,331 @@
+/*
+ * Copyright (c) 2018 Robin Krahl <robin.krahl@ireas.org>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <stdbool.h> /* libnitrokey needs stdbool.h */
+#include <libnitrokey/NK_C_API.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <time.h>
+
+#include "options.h"
+
+static const size_t MAX_PWLEN = 25;
+static const size_t MAX_TMP_PWLEN = 25;
+
+/* operation modes */
+static int mod_generate(struct options *options);
+
+/* device communication */
+static int authenticate_user(char *tmp_pw, size_t len);
+static int connect(enum model model);
+static int otp_needs_password(void);
+static int otp_generate(enum algorithm alg, unsigned int slot,
+ const char **otp);
+static int otp_generate_password(enum algorithm alg, unsigned int slot,
+ const char **otp);
+
+/* helpers */
+static int generate_tmp_password(char *tmp_pw, size_t len);
+static int read_user_password(char *pw, size_t len);
+static int read_password(const char *prompt, char *pw, size_t len,
+ FILE *stream);
+static int read_password_line(char *pw, size_t len, FILE *stream);
+
+int main(int argc, char **argv)
+{
+ int err;
+ struct options options = {0};
+
+ err = parse_options(argc, argv, &options);
+ if (err > 0)
+ goto cleanup;
+
+ switch (options.mode) {
+ case MODE_GENERATE:
+ err = mod_generate(&options);
+ break;
+ case MODE_NONE:
+ break;
+ }
+
+cleanup:
+ free_options(&options);
+
+ return err;
+}
+
+static int mod_generate(struct options *options)
+{
+ int err = 0;
+ const char *otp = NULL;
+
+ err = connect(options->model);
+ if (err)
+ return err;
+
+ if (otp_needs_password())
+ err = otp_generate_password(options->alg, options->slot, &otp);
+ else
+ err = otp_generate(options->alg, options->slot, &otp);
+
+ if (!err)
+ printf("%s\n", otp);
+
+ free((char *) otp);
+
+ return err;
+}
+
+static int authenticate_user(char *tmp_pw, size_t len)
+{
+ int err = 0;
+ char pw[MAX_PWLEN];
+
+ err = read_user_password(pw, MAX_PWLEN);
+ if (err)
+ return err;
+
+ err = generate_tmp_password(tmp_pw, len);
+ if (err)
+ goto cleanup;
+
+ err = NK_user_authenticate(pw, tmp_pw);
+ if (err) {
+ memset(tmp_pw, 0, len);
+ fprintf(stderr, "User authentication failed.\n");
+ }
+
+cleanup:
+ memset(pw, 0, MAX_PWLEN);
+
+ return err;
+}
+
+static int connect(enum model model)
+{
+ int nk_err = 0;
+
+ switch (model) {
+ case MODEL_AUTO:
+ printf("Trying to connect in automatic mode ...\n");
+ nk_err = NK_login_auto();
+ break;
+ case MODEL_PRO:
+ printf("Trying to connect to a Nitrokey Pro ...\n");
+ nk_err = NK_login("P");
+ break;
+ case MODEL_STORAGE:
+ printf("Trying to connect to a Nitrokey Storage ...\n");
+ nk_err = NK_login("S");
+ break;
+ case MODEL_DEFAULT:
+ default:
+ fprintf(stderr, "Programming error: invalid model.\n");
+ return 1;
+ }
+
+ if (nk_err == 0) {
+ fprintf(stderr, "Could not connect to the Nitrokey device.\n");
+ return 1;
+ }
+
+ printf("successfully connected.\n");
+
+ return 0;
+}
+
+static int otp_needs_password(void)
+{
+ /*
+ * config = {numlock, capslock, scrollock, enable_user_password,
+ * disable_user_password}
+ */
+ const uint8_t *config = NK_read_config();
+ return config[3];
+}
+
+static int otp_generate(enum algorithm alg, unsigned int slot,
+ const char **otp)
+{
+ int err = 0;
+
+ *otp = NULL;
+ switch (alg) {
+ case ALGORITHM_HOTP:
+ if (slot < 1 || slot > MAX_SLOT_HOTP) {
+ fprintf(stderr, "Programming error: invalid slot.\n");
+ err = 1;
+ } else {
+ *otp = NK_get_hotp_code(slot);
+ }
+ break;
+ case ALGORITHM_TOTP:
+ if (slot < 1 || slot > MAX_SLOT_TOTP) {
+ fprintf(stderr, "Programming error: invalid slot.\n");
+ err = 1;
+ } else {
+ /* TODO: pass appropriate values */
+ *otp = NK_get_totp_code(slot, 0, 0, 0);
+ }
+ break;
+ default:
+ fprintf(stderr, "Programming error: invalid algorithm.\n");
+ err = 1;
+ }
+
+ return err;
+}
+
+static int otp_generate_password(enum algorithm alg, unsigned int slot,
+ const char **otp)
+{
+ int err = 0;
+ char tmp_pw[MAX_TMP_PWLEN];
+
+ err = authenticate_user(tmp_pw, MAX_TMP_PWLEN);
+ if (err)
+ return err;
+
+ *otp = NULL;
+ switch (alg) {
+ case ALGORITHM_HOTP:
+ if (slot < 1 || slot > MAX_SLOT_HOTP) {
+ fprintf(stderr, "Programming error: invalid slot.\n");
+ err = 1;
+ } else {
+ *otp = NK_get_hotp_code_PIN(slot, tmp_pw);
+ }
+ break;
+ case ALGORITHM_TOTP:
+ if (slot < 1 || slot > MAX_SLOT_TOTP) {
+ fprintf(stderr, "Programming error: invalid slot.\n");
+ err = 1;
+ } else {
+ /* TODO: pass appropriate values */
+ *otp = NK_get_totp_code_PIN(slot, 0, 0, 0, tmp_pw);
+ }
+ break;
+ default:
+ fprintf(stderr, "Programming error: invalid algorithm.\n");
+ err = 1;
+ }
+
+ memset(tmp_pw, 0, MAX_TMP_PWLEN);
+
+ return err;
+}
+
+static int generate_tmp_password(char *tmp_pw, size_t len)
+{
+ static const char *charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn"
+ "opqrstuvwxyz0123456789";
+ size_t charset_len = strlen(charset);
+
+ /* TODO: use a better seed, maybe even a better generator */
+ srandom(time(NULL));
+
+ for (size_t i = 0; i < len - 1; i++) {
+ size_t j = random() / (RAND_MAX / charset_len);
+ tmp_pw[i] = charset[j];
+ }
+ tmp_pw[len - 1] = '\0';
+
+ return 0;
+}
+
+static int read_user_password(char *pw, size_t len)
+{
+ int err = 0;
+ char *env = NULL;
+
+ env = getenv("NKOTP_USER_PASSWORD");
+ if (env) {
+ /* TODO: rethink policy for overlong password inputs */
+ strncpy(pw, env, len);
+ if (pw[len - 1] != '\0') {
+ memset(pw, 0, len);
+ fprintf(stderr, "The user password is too long.\n");
+ err = 1;
+ }
+ } else {
+ err = read_password("User password: ", pw, len, stdin);
+ }
+
+ return err;
+}
+
+static int read_password(const char *prompt, char *pw, size_t len,
+ FILE *stream)
+{
+ int err = 0;
+ struct termios old, new;
+
+ /* get and store current terminal state */
+ err = tcgetattr(fileno(stdin), &old);
+ if (err)
+ goto cleanup;
+
+ /* surpress echo for password input */
+ new = old;
+ new.c_lflag &= ~ECHO;
+ err = tcsetattr(fileno(stdin), TCSAFLUSH, &new);
+ if (err)
+ goto cleanup;
+
+ printf(prompt);
+ fflush(stdout);
+
+ /* TODO: rethink policy for overlong password inputs */
+ err = read_password_line(pw, len, stream);
+
+ /* reset terminal state */
+ tcsetattr(fileno(stdin), TCSAFLUSH, &old);
+
+cleanup:
+ if (err)
+ fprintf(stderr, "Could not read password. Aborting.\n");
+
+ return err;
+}
+
+static int read_password_line(char *pw, size_t len, FILE *stream)
+{
+ int err = 0;
+ char *line = NULL;
+ size_t n = 0;
+ int nread = 0;
+
+ nread = getline(&line, &n, stdin);
+ /* no echo, so manually print the newline before doing anything */
+ printf("\n");
+ if (nread < 0) {
+ fprintf(stderr, "Could not read user input.\n");
+ err = 1;
+ goto cleanup;
+ }
+ if (nread == 0) {
+ fprintf(stderr, "Empty password provided. Aborting.\n");
+ err = 1;
+ goto cleanup;
+ }
+ line[nread - 1] = '\0';
+
+ /* TODO: rethink policy for overlong password inputs */
+ strncpy(pw, line, len);
+ if (pw[len - 1] != '\0') {
+ memset(pw, 0, len);
+ fprintf(stderr, "The user password is too long.\n");
+ err = 1;
+ }
+
+cleanup:
+ free(line);
+
+ return err;
+}
diff --git a/options.c b/options.c
new file mode 100644
index 0000000..3f82560
--- /dev/null
+++ b/options.c
@@ -0,0 +1,436 @@
+/*
+ * Copyright (c) 2018 Robin Krahl <robin.krahl@ireas.org>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include "options.h"
+
+#include <confuse.h>
+#include <getopt.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+static cfg_opt_t cfg_opts[] = {
+ CFG_STR("algorithm", NULL, CFGF_NONE),
+ CFG_STR("model", NULL, CFGF_NONE),
+ CFG_INT("slot", 0, CFGF_NONE),
+ CFG_END()
+};
+
+static const char *optstr = "a:c:m:s:ghv";
+static struct option long_options[] = {
+ {"algorithm", required_argument, 0, 'a'},
+ {"config", required_argument, 0, 'c'},
+ {"model", required_argument, 0, 'm'},
+ {"slot", required_argument, 0, 's'},
+ {"generate", no_argument, 0, 'g'},
+ {"help", no_argument, 0, 'h'},
+ {"version", no_argument, 0, 'v'},
+ {0}
+};
+
+static const char *DESCRIPTION =
+"nkotp -- one-time password generator for Nitrokey devices\n";
+static const char *USAGE =
+"Usage: nkotp [options] --generate | --help | --version\n";
+static const char *OPTIONS_GENERAL = "General options:\n"
+" -a algorithm, --algorithm algorithm\n"
+" set the OTP algorithm ('h' for HTOP, 't' for TOPT, default: 't')\n"
+" -c file, --config file\n"
+" set the configuration file\n"
+" -m model, --model model\n"
+" set the model to connect to ('a' for automatic, 'p' for Nitrokey\n"
+" Pro, 's' for Nitrokey Storage, default: 'a')\n"
+" -s slot, --slot slot\n"
+" set the OTP slot (default: 1)\n";
+static const char *OPTIONS_MODES = "Modes of operation:\n"
+" -g, --generate\n"
+" generate a one-time password\n"
+" -h, --help\n"
+" print this help message\n"
+" -v, --version\n"
+" print version information\n";
+
+static const char *ERRMSG_ALGORITHM = "Unsupported algorithm '%s'. Supported "
+"values are 'h' (HTOP) and 't' (TOTP).\n";
+static const char *ERRMSG_MODEL = "Unsupported model '%s'. Supported values "
+"are 'a' (automatic), 'p' (Nitrokey Pro) and 's' (Nitrokey Storage).\n";
+
+/* parse options from cli, config file or environment variables */
+static int parse_options_cli(int argc, char **argv, struct options *options);
+static int parse_options_config(struct options *options);
+static int parse_options_env(struct options *options);
+
+/* print help, usage or version messages */
+static void print_help(void);
+static void print_usage(void);
+static void print_version(void);
+
+/* set options */
+static int set_algorithm(struct options *options, const char *arg);
+static int set_config(struct options *options, const char *arg);
+static int set_mode(struct options *options, enum mode mode);
+static int set_model(struct options *options, const char *arg);
+static int set_slot(struct options *options, int arg);
+
+/* helpers */
+static char *get_home(void);
+static char *get_xdg_config_home(void);
+static char *get_default_config_file(void);
+
+int parse_options(int argc, char **argv, struct options *options)
+{
+ int err = 0;
+
+ if (!options)
+ return 1;
+
+ *options = (struct options) {0};
+
+ err = parse_options_cli(argc, argv, options);
+ if (!err)
+ err = parse_options_config(options);
+ if (!err)
+ err = parse_options_env(options);
+
+ /* set defaults */
+ if (options->alg == ALGORITHM_DEFAULT)
+ options->alg = ALGORITHM_TOTP;
+ if (options->model == MODEL_DEFAULT)
+ options->model = MODEL_AUTO;
+ if (options->slot == 0)
+ options->slot = 1;
+
+ /* check consistency */
+ if (options->slot < 0) {
+ fprintf(stderr, "The OTP slot must be positive!\n");
+ err = 1;
+ }
+ switch (options->alg) {
+ case ALGORITHM_HOTP:
+ if (options->slot > MAX_SLOT_HOTP) {
+ fprintf(stderr, "Nitrokey only supports %lu HOTP "
+ "slots.\n", MAX_SLOT_HOTP);
+ err = 1;
+ }
+ break;
+ case ALGORITHM_TOTP:
+ if (options->slot > MAX_SLOT_TOTP) {
+ fprintf(stderr, "Nitrokey only supports %lu TOTP "
+ "slots.\n", MAX_SLOT_TOTP);
+ err = 1;
+ }
+ break;
+ case ALGORITHM_DEFAULT:
+ default:
+ fprintf(stderr, "Programming error: invalid algorithm.\n");
+ err = 1;
+ }
+
+ return err;
+}
+
+void free_options(struct options *options)
+{
+ if (!options)
+ return;
+
+ if (options->cfg_file)
+ free(options->cfg_file);
+}
+
+static int parse_options_cli(int argc, char **argv, struct options *options)
+{
+ int err = 0;
+ int c;
+
+ while ((c = getopt_long(argc, argv, optstr, long_options, NULL))
+ != -1 && !err) {
+ switch (c) {
+ case 'a':
+ err = set_algorithm(options, optarg);
+ break;
+ case 'c':
+ err = set_config(options, optarg);
+ break;
+ case 'g':
+ err = set_mode(options, MODE_GENERATE);
+ break;
+ case 'h':
+ print_help();
+ return 0;
+ case 'm':
+ err = set_model(options, optarg);
+ break;
+ case 's':
+ err = set_slot(options, atoi(optarg));
+ break;
+ case 'v':
+ print_version();
+ return 0;
+ case '?':
+ print_usage();
+ return 1;
+ default:
+ fprintf(stderr, "Unexpected character code 0%o "
+ "returned by getopt.\n", c);
+ return 1;
+ }
+ }
+
+ if (!err) {
+ if (optind < argc || options->mode == MODE_NONE) {
+ print_usage();
+ err = 1;
+ }
+ }
+
+ return err;
+}
+
+static int parse_options_config(struct options *options)
+{
+ /* try to read config file from environment */
+ if (!options->cfg_file) {
+ char *env = getenv("NKOTP_CONFIG");
+ if (env)
+ options->cfg_file = strdup(env);
+ }
+
+ /* try to use the default config file */
+ if (!options->cfg_file) {
+ char *def_cfg_file = get_default_config_file();
+ if (def_cfg_file) {
+ struct stat s;
+ if (stat(def_cfg_file, &s) == 0) {
+ options->cfg_file = def_cfg_file;
+ } else {
+ free(def_cfg_file);
+ }
+ }
+ }
+
+ /* stop if no config file has been set */
+ if (!options->cfg_file)
+ return 0;
+
+ /* read config file */
+ int err = 0;
+ cfg_t *cfg = cfg_init(cfg_opts, CFGF_NONE);
+ if (cfg_parse(cfg, options->cfg_file) == CFG_PARSE_ERROR) {
+ err = 1;
+ goto cleanup;
+ }
+
+ if (options->alg == ALGORITHM_DEFAULT) {
+ char *val = cfg_getstr(cfg, "algorithm");
+ if (val)
+ err |= set_algorithm(options, val);
+ }
+
+ if (options->model == MODEL_DEFAULT) {
+ char *val = cfg_getstr(cfg, "model");
+ if (val)
+ err |= set_model(options, val);
+ }
+
+ if (options->slot == 0) {
+ int val = cfg_getint(cfg, "slot");
+ if (val)
+ err |= set_slot(options, val);
+ }
+
+cleanup:
+ cfg_free(cfg);
+ return err;
+}
+
+static int parse_options_env(struct options *options)
+{
+ int err = 0;
+ char *env;
+
+ if (options->alg == ALGORITHM_DEFAULT) {
+ env = getenv("NKOTP_ALGORITHM");
+ if (env)
+ err |= set_algorithm(options, env);
+ }
+
+ if (options->model == MODEL_DEFAULT) {
+ env = getenv("NKOTP_MODEL");
+ if (env)
+ err |= set_model(options, env);
+ }
+
+ if (options->slot == 0) {
+ env = getenv("NKOTP_SLOT");
+ if (env)
+ err |= set_slot(options, atoi(env));
+ }
+
+ return 0;
+}
+
+static void print_help(void)
+{
+ printf(DESCRIPTION);
+ printf("\n");
+ printf(USAGE);
+ printf("\n");
+ printf(OPTIONS_GENERAL);
+ printf("\n");
+ printf(OPTIONS_MODES);
+}
+
+static void print_usage(void)
+{
+ fprintf(stderr, USAGE);
+}
+
+static void print_version(void)
+{
+ printf("nkotp v" NKOTP_VERSION "\n");
+}
+
+static int set_algorithm(struct options *options, const char *arg)
+{
+ if (strlen(arg) != 1) {
+ fprintf(stderr, ERRMSG_ALGORITHM, arg);
+ return 1;
+ }
+
+ switch (arg[0]) {
+ case 'h':
+ options->alg = ALGORITHM_HOTP;
+ break;
+ case 't':
+ options->alg = ALGORITHM_TOTP;
+ break;
+ default:
+ fprintf(stderr, ERRMSG_ALGORITHM, arg);
+ return 1;
+ }
+
+ return 0;
+}
+
+static int set_config(struct options *options, const char *arg)
+{
+ options->cfg_file = strdup(arg);
+ if (!options->cfg_file) {
+ fprintf(stderr, "Out of memory.\n");
+ return 1;
+ }
+ return 0;
+}
+
+static int set_mode(struct options *options, enum mode mode)
+{
+ if (options->mode != MODE_NONE) {
+ fprintf(stderr, "You may only set one mode of operation.\n");
+ return 1;
+ }
+ options->mode = mode;
+ return 0;
+}
+
+static int set_model(struct options *options, const char *arg)
+{
+ if (strlen(arg) != 1) {
+ fprintf(stderr, ERRMSG_MODEL, arg);
+ return 1;
+ }
+
+ switch (arg[0]) {
+ case 'a':
+ options->model = MODEL_AUTO;
+ break;
+ case 's':
+ options->model = MODEL_STORAGE;
+ break;
+ case 'p':
+ options->model = MODEL_PRO;
+ break;
+ default:
+ fprintf(stderr, ERRMSG_MODEL, arg);
+ return 1;
+ }
+
+ return 0;
+}
+
+static int set_slot(struct options *options, int arg)
+{
+ if (arg <= 0) {
+ fprintf(stderr, "The slot must be positive.\n");
+ return 1;
+ }
+ options->slot = arg;
+ return 0;
+}
+
+static char *get_home(void)
+{
+ char *env = getenv("HOME");
+ if (env)
+ return strdup(env);
+
+ struct passwd *pw = getpwuid(getuid());
+ return strdup(pw->pw_dir);
+}
+
+static char *get_xdg_config_home(void)
+{
+ char *env = getenv("XDG_CONFIG_HOME");
+ if (env)
+ return strdup(env);
+
+ char *home = get_home();
+ char *path = NULL;
+ if (home) {
+ int len = snprintf(NULL, 0, "%s/.config", home) + 1;
+ if (len <= 0)
+ goto cleanup;
+
+ path = calloc(sizeof *path, len);
+ if (!path)
+ goto cleanup;
+ if (snprintf(path, len, "%s/.config", home) + 1 != len) {
+ free(path);
+ path = NULL;
+ }
+ }
+
+cleanup:
+ free(home);
+ return path;
+}
+
+static char *get_default_config_file(void)
+{
+ char *config_home = get_xdg_config_home();
+ if (!config_home)
+ return NULL;
+
+ char *file = NULL;
+ int len = snprintf(NULL, 0, "%s/nkotp/config", config_home) + 1;
+ if (len <= 0)
+ goto cleanup;
+
+ file = calloc(sizeof *file, len);
+ if (!file)
+ goto cleanup;
+ if (snprintf(file, len, "%s/nkotp/config", config_home) + 1 != len) {
+ free(file);
+ file = NULL;
+ }
+
+cleanup:
+ free(config_home);
+ return file;
+}
diff --git a/options.h b/options.h
new file mode 100644
index 0000000..828c680
--- /dev/null
+++ b/options.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2018 Robin Krahl <robin.krahl@ireas.org>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef NKOTP_OPTIONS_H
+#define NKOTP_OPTIONS_H
+
+#include <stddef.h>
+
+enum mode {
+ MODE_NONE,
+ MODE_GENERATE
+};
+
+enum algorithm {
+ ALGORITHM_DEFAULT,
+ ALGORITHM_HOTP,
+ ALGORITHM_TOTP
+};
+
+enum model {
+ MODEL_DEFAULT,
+ MODEL_AUTO,
+ MODEL_PRO,
+ MODEL_STORAGE
+};
+
+struct options {
+ enum algorithm alg;
+ char *cfg_file;
+ char model;
+ int slot;
+ enum mode mode;
+};
+
+static const size_t MAX_SLOT_HOTP = 3;
+static const size_t MAX_SLOT_TOTP = 15;
+
+int parse_options(int argc, char **argv, struct options *options);
+void free_options(struct options *options);
+
+#endif