From: mar77i Date: Tue, 24 Mar 2026 01:38:14 +0000 (+0100) Subject: rewrite totp. X-Git-Url: https://git.mar77i.info/?a=commitdiff_plain;p=ga-cli rewrite totp. --- diff --git a/Makefile b/Makefile index 1a49147..d19027d 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,10 @@ - -CFLAGS += -D_DEFAULT_SOURCE $(DEV_CFLAGS) -LDFLAGS += $(DEV_LDFLAGS) +CFLAGS += -D_DEFAULT_SOURCE $(PROD_CFLAGS) +LDFLAGS += $(PROD_LDFLAGS) LDLIBS += -lcrypto -totp: totp.o totp_args.o +all: totp -all: +totp: totp.o parse_args.o include global.mk diff --git a/parse_args.c b/parse_args.c new file mode 100644 index 0000000..9f2fff1 --- /dev/null +++ b/parse_args.c @@ -0,0 +1,64 @@ + +/* parse_args.c + * + * Copyright (c) 2017-2026, mar77i + * + * This software may be modified and distributed under the terms + * of the ISC license. See the LICENSE file for details. + */ + +#include +#include +#include + +static inline void print_help_and_exit(const char *argv0) { + printf( + ( + "Usage: %s -h|--help | [--] SEARCH\n" + "\n" + "Options:\n" + "-h, --help display this help\n" + "SEARCH Search secrets for a substring case-insensitively\n" + "\n" + "Environment:\n" + "OTP_SECRETS specify secrets file. Defaults to ~/.otp_secrets\n" + "\n" + ), + argv0 + ); + exit(0); +} + +static inline int get_default_infile(char **infile) { + size_t len; + char *home; + if ((home = getenv("HOME")) == NULL) { + fprintf(stderr, "Error: $HOME not set. Could not find secrets file.\n"); + return -1; + } + len = strlen(home) + 14; + *infile = malloc(len); + if (*infile == NULL) { + perror("malloc"); + return -1; + } + snprintf(*infile, len, "%s/.otp_secrets", home); + return 0; +} + +int parse_args(int argc, char **argv, char **infile, char **search) { + if (argc == 2) { + if (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) + print_help_and_exit(*argv); + else + *search = argv[1]; + } else if (argc == 3 && strcmp(argv[1], "--") == 0) + *search = argv[2]; + else if (argc != 1) { + fprintf(stderr, "Error: invalid arguments.\n"); + return -1; + } + if ((*infile = getenv("OTP_SECRETS")) == NULL) + return get_default_infile(infile); + return 0; +} diff --git a/parse_args.h b/parse_args.h new file mode 100644 index 0000000..3cc51f5 --- /dev/null +++ b/parse_args.h @@ -0,0 +1,15 @@ + +/* parse_args.h + * + * Copyright (c) 2017-2026, mar77i + * + * This software may be modified and distributed under the terms + * of the ISC license. See the LICENSE file for details. + */ + +#ifndef PARSE_ARGS_H +#define PARSE_ARGS_H + +int parse_args(int argc, char **argv, char **infile, char **search); + +#endif // PARSE_ARGS_H diff --git a/totp.c b/totp.c index 27138d0..2401a38 100644 --- a/totp.c +++ b/totp.c @@ -1,169 +1,178 @@ /* totp.c * - * Copyright (c) 2017-2024, mar77i + * Copyright (c) 2017-2026, mar77i * * This software may be modified and distributed under the terms * of the ISC license. See the LICENSE file for details. */ #include +#include +#include #include -#include -#include +#include #include #include #include +#include #include +#include -#include "totp_args.h" +#include "parse_args.h" #define INTERVAL_SECONDS 30 -static inline char *b32_advance_inptr(char *inptr) { - while (isspace(*inptr) || *inptr == '\n') - inptr++; - return inptr; +static inline int posix_close(int fd) { + if (close(fd) == 0) + return 0; + perror("close"); + if (errno != EINTR) + return -1; + if (close(fd) < 0) { + perror("close"); + return -1; + } + return 0; } -static inline uint8_t b32_decode_char(uint8_t c) { - const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - char *ptr = strchr(alphabet, toupper(c)); - if (ptr == NULL || *ptr == '\0') { - fprintf( - stderr, - "Error: %s: invalid input: '%c' (%02X)\n", - __func__, - c, - c - ); - return UINT8_MAX; +static inline int read_secrets(const char *infile, char *buffer, size_t size) { + ssize_t r; + int fd; + if ((fd = open(infile, O_RDONLY)) < 0) { + perror("open"); + return -1; } - return ptr - alphabet; + if ((r = read(fd, buffer, size)) < 0) { + perror("read"); + close(fd); + return -1; + } + if (posix_close(fd) < 0) + return -1; + buffer[r] = '\0'; + return 0; +} + +static inline int b32_get_next_char(char **inptr) { + while (**inptr == ' ' || **inptr == '\t') + (*inptr)++; + return **inptr != '\n' && **inptr != '\0' && **inptr != '='; } static inline int b32_decode(char *buf, size_t *len) { - size_t used; - uint8_t x; - char *outptr = buf, *inptr = buf; - for (used = 0;; used = (used + 1) & 7) { - inptr = b32_advance_inptr(inptr); - if (*inptr == '\0' || *inptr == '=') - break; - x = b32_decode_char(*inptr++); - if (x == UINT8_MAX) + const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + uint8_t bits, *outptr = (uint8_t*)buf; + char *inptr = buf, *ptr, shift; + /* Cycle through {-4..3} in intervals of 3 (== -5): + * { 3, -2, 1, -4, -1, 2, -3, 0 } + * This corresponds to how the shift offsets for decoded bits unfold. + */ + for (shift = 3; b32_get_next_char(&inptr); shift += 3 - (shift > 0) * 8) { + ptr = strchr(alphabet, toupper(*inptr)); + if (ptr == NULL) { + fprintf( + stderr, + "Error: invalid input: '%c' (%02X)\n", + *inptr, + *inptr & 0xff + ); return -1; - switch (used) { + } + inptr++; + bits = ptr - alphabet; + switch (shift) { case 0: - *outptr = x << 3; + *outptr++ |= bits; break; case 1: - *outptr |= x >> 2; - outptr++; - *outptr = x << 6; - break; case 2: - *outptr |= x << 1; + *outptr |= bits << shift; break; case 3: - *outptr |= x >> 4; - outptr++; - *outptr = x << 4; - break; - case 4: - *outptr |= x >> 1; - outptr++; - *outptr = x << 7; + *outptr = bits << 3; break; - case 5: - *outptr |= x << 2; - break; - case 6: - *outptr |= x >> 3; - outptr++; - *outptr = x << 5; - break; - case 7: - *outptr |= x; - outptr++; + default: + *outptr |= bits >> -shift; + *++outptr = bits << (shift + 8); break; } } if (len != NULL) - *len = outptr - buf; + *len = outptr - (uint8_t*)buf; return 0; } -static inline uint8_t *pack_be64(uint8_t data[], uint64_t value) { +static inline void pack_big_endian64(uint8_t data[], uint64_t value) { size_t i; for (i = 0; i < sizeof value; i++) data[i] = value >> ((sizeof value - 1 - i) * CHAR_BIT); - return data; } -static inline int calculate_totp(const char *line, const char *search) { +int32_t calculate_totp(char *secret) { size_t len = 0; - unsigned int hmac_len = 0, label_len; - uint8_t data[sizeof (uint64_t)], i, *ptr8; - char *find, *ptr = line ? strrchr(line, ':') : NULL; - if (ptr == NULL) + uint8_t data[sizeof (time_t)], md[20], i; + if (b32_decode(secret, &len) < 0) return -1; - if (strlen(line) < 2) - return 0; - label_len = ptr - line; - if (search != NULL) { - find = strcasestr(line, search); - if (find == NULL || find + strlen(search) > ptr) - return 0; - } - ptr++; - if (b32_decode(ptr, &len) < 0) - return -1; - // fill data with the current time divided by INTERVAL_SECONDS - pack_be64(data, time(NULL) / INTERVAL_SECONDS); - ptr8 = (uint8_t*)ptr; - if ( - HMAC(EVP_sha1(), ptr, len, data, sizeof data, ptr8, &hmac_len) == NULL - ) { - fprintf(stderr, "\nError: hmac.\n"); + pack_big_endian64(data, time(NULL) / INTERVAL_SECONDS); + if (HMAC(EVP_sha1(), secret, len, data, sizeof data, md, NULL) == NULL) { + fprintf(stderr, "\nError: HMAC.\n"); return -1; } - i = ptr8[hmac_len - 1] & 0xf; - printf( - "%.*s: %06"PRIu32"\n", - label_len, - line, - ( - ((ptr8[i] & 0x7f) << 24) - | (ptr8[i + 1] << 16) - | (ptr8[i + 2] << 8) - | ptr8[i + 3] - ) % 1000000 - ); + i = md[19] & 0xf; + return ( + ((md[i] & 0x7f) << 24) + | (md[i + 1] << 16) + | (md[i + 2] << 8) + | md[i + 3] + ) % 1000000; +} + +static inline int totp(const char *infile, const char *search, size_t size) { + int32_t result = 0; + int label_len, search_len = search != NULL ? strlen(search) : 0; + const char *f; + char buffer[size + 1], *line, *secret, *end; + if (read_secrets(infile, buffer, size) < 0) + return -1; + line = buffer; + for (line = buffer; line != NULL; line = end + (end != NULL)) { + secret = strchr(line, ':'); + end = strchr(line, '\n'); + if (secret == NULL || (end != NULL && end < secret)) + continue; + label_len = secret - line; + if ( + search == NULL + || ( + (f = strstr(line, search)) != NULL + && f < line + label_len - search_len + ) + ) { + if ((result = calculate_totp(secret + 1)) < 0) + return -1; + printf("%.*s: %06"PRIu32"\n", label_len, line, result); + } + } while (line != NULL); return 0; } int main(int argc, char *argv[]) { - struct totp_args ta = TOTP_ARGS_INIT(argc, argv); + struct stat st; int ret = EXIT_FAILURE; - char buf[PATH_MAX]; - switch (totp_args_setup(&ta)) { - case 1: - ret = EXIT_SUCCESS; - // FALLTHROUGH - case -1: + char *infile = NULL, *search = NULL; + if (parse_args(argc, argv, &infile, &search) < 0) + return EXIT_FAILURE; + if (stat(infile, &st) < 0) { + perror("stat"); goto error; } - while (fgets(buf, sizeof buf, ta.fh) != NULL) - if (calculate_totp(buf, ta.search) < 0) - goto error; - if (ferror(ta.fh)) { - fprintf(stderr, "Error: read error.\n"); + if (totp(infile, search, st.st_size) < 0) goto error; - } ret = EXIT_SUCCESS; error: - totp_args_cleanup(ta); + if (getenv("OTP_SECRETS") != infile) + free(infile); return ret; } diff --git a/totp_args.c b/totp_args.c deleted file mode 100644 index 4810a04..0000000 --- a/totp_args.c +++ /dev/null @@ -1,140 +0,0 @@ - -/* totp_args.c - * - * Copyright (c) 2024, mar77i - * - * This software may be modified and distributed under the terms - * of the ISC license. See the LICENSE file for details. - */ - -#include -#include - -#include "totp_args.h" - -static inline int print_help(const char *argv0) { - printf( - "Usage: %s -h|--help\n" - " %s [-f] [--] [search]\n" - "\n" - "Options:\n" - "-h, --help: display this help\n" - "-f: specify input file.\n" - "search: output filter string.\n" - "\n" - "Environment:\n" - "OTP_SECRETS set a default input file (defaults to ~/.otp_secrets)\n" - "\n", - argv0, - argv0 - ); - return 1; -} - -#define ARG (ta->argv[ta->argi]) -static inline int parse_arg_file(struct totp_args *ta) { - char *ptr = ARG + 2; - if (*ptr == '\0') { - if (++ta->argi >= ta->argc) { - fprintf(stderr, "Error: filename missing.\n"); - return -1; - } - ptr = ARG; - } - ta->path = strdup(ptr); - if (ta->path == NULL) { - perror("strdup"); - return -1; - } - return 0; -} - -#define TOTP_DEFAULT_FILE "/.otp_secrets" -static inline char *get_otp_secrets_path(void) { - size_t len; - char *ptr, *env = getenv("OTP_SECRETS"); - if (env == NULL) - goto default_path; - len = strlen(env); - if (len == 0) { - fprintf(stderr, "Error: path is empty: OTP_SECRETS\n"); - return NULL; - } - ptr = strdup(env); - if (ptr == NULL) - perror("strdup"); - return ptr; -default_path: - env = getenv("HOME"); - if (env == NULL) { - fprintf(stderr, "Error: HOME is unset.\n"); - return NULL; - } else if (*env == '\0') { - fprintf(stderr, "Error: HOME is empty.\n"); - return NULL; - } - len = strlen(env); - while (env[--len] == '/'); - len++; - ptr = malloc(len + sizeof TOTP_DEFAULT_FILE); - if (ptr != NULL) { - memcpy(ptr, env, len); - memcpy(ptr + len, TOTP_DEFAULT_FILE, sizeof TOTP_DEFAULT_FILE); - } else - perror("malloc"); - return ptr; -} - -static inline int parse_arg(struct totp_args *ta) { - if (ta->double_minus != 0) - goto double_minus_set; - if (!strcmp(ARG, "-h") || !strcmp(ARG, "--help")) - return print_help(ta->argv[0]); - else if (!strncmp(ARG, "-f", 2)) - return parse_arg_file(ta); - else if (!strcmp(ARG, "--")) { - ta->double_minus = 1; - return 0; - } -double_minus_set: - if (ta->search != NULL) { - fprintf(stderr, "Error: search string has already been passed.\n"); - return -1; - } - ta->search = ARG; - return 0; -} - -int totp_args_setup(struct totp_args *ta) { - int ret = -1; - for (; ta->argi < ta->argc; ta->argi++) - switch (parse_arg(ta)) { - case 1: - ret = 1; - // FALLTHROUGH - case -1: - goto error; - } - if (ta->path == NULL) - ta->path = get_otp_secrets_path(); - if (ta->path == NULL) - goto error; - if (strcmp(ta->path, "-")) { - ta->fh = fopen(ta->path, "rb"); - if (ta->fh == NULL) { - perror("fopen"); - goto error; - } - } else - ta->fh = stdin; - ret = 0; -error: - return ret; -} -#undef ARG - -void totp_args_cleanup(const struct totp_args ta) { - if (ta.fh != NULL && ta.fh != stdin) - fclose(ta.fh); - free(ta.path); -} diff --git a/totp_args.h b/totp_args.h deleted file mode 100644 index 25a2d53..0000000 --- a/totp_args.h +++ /dev/null @@ -1,34 +0,0 @@ - -/* totp_args.h - * - * Copyright (c) 2024, mar77i - * - * This software may be modified and distributed under the terms - * of the ISC license. See the LICENSE file for details. - */ - -#ifndef TOTP_ARGS_H -#define TOTP_ARGS_H - -#include - -struct totp_args { - FILE *fh; - int argc, argi; - char **argv, *path, *search, double_minus; -}; - -#define TOTP_ARGS_INIT(c, v) ((struct totp_args){ \ - .fh = NULL, \ - .argc = (c), \ - .argi = 1, \ - .argv = (v), \ - .path = NULL, \ - .search = NULL, \ - .double_minus = 0, \ -}) - -int totp_args_setup(struct totp_args *ta); -void totp_args_cleanup(const struct totp_args ta); - -#endif // TOTP_ARGS_H