diff options
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | LICENSE | 21 | ||||
-rw-r--r-- | Makefile | 32 | ||||
-rw-r--r-- | README.md | 49 | ||||
-rw-r--r-- | TODO.md | 12 | ||||
-rw-r--r-- | config.mk | 18 | ||||
-rw-r--r-- | nkotp.1.pod | 141 | ||||
-rw-r--r-- | nkotp.c | 331 | ||||
-rw-r--r-- | options.c | 436 | ||||
-rw-r--r-- | options.h | 44 |
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 @@ -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 @@ -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> @@ -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 |