--- /dev/null
+
+/* parse_args.c
+ *
+ * Copyright (c) 2017-2026, mar77i <mar77i at protonmail dot ch>
+ *
+ * This software may be modified and distributed under the terms
+ * of the ISC license. See the LICENSE file for details.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+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;
+}
/* totp.c
*
- * Copyright (c) 2017-2024, mar77i <mar77i at protonmail dot ch>
+ * Copyright (c) 2017-2026, mar77i <mar77i at protonmail dot ch>
*
* This software may be modified and distributed under the terms
* of the ISC license. See the LICENSE file for details.
*/
#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
#include <inttypes.h>
-#include <limits.h>
-#include <stdint.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
+#include <unistd.h>
#include <openssl/hmac.h>
+#include <sys/stat.h>
-#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;
}
+++ /dev/null
-
-/* totp_args.c
- *
- * Copyright (c) 2024, mar77i <mar77i at protonmail dot ch>
- *
- * This software may be modified and distributed under the terms
- * of the ISC license. See the LICENSE file for details.
- */
-
-#include <stdlib.h>
-#include <string.h>
-
-#include "totp_args.h"
-
-static inline int print_help(const char *argv0) {
- printf(
- "Usage: %s -h|--help\n"
- " %s [-f<file>] [--] [search]\n"
- "\n"
- "Options:\n"
- "-h, --help: display this help\n"
- "-f<file>: 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);
-}