]> git.mar77i.info Git - ga-cli/commitdiff
rewrite totp. master
authormar77i <mar77i@protonmail.ch>
Tue, 24 Mar 2026 01:38:14 +0000 (02:38 +0100)
committermar77i <mar77i@protonmail.ch>
Tue, 24 Mar 2026 01:38:14 +0000 (02:38 +0100)
Makefile
parse_args.c [new file with mode: 0644]
parse_args.h [new file with mode: 0644]
totp.c
totp_args.c [deleted file]
totp_args.h [deleted file]

index 1a491476e88e25f3c79ed10334fe084c0d4d2a75..d19027dc6b439635a9a164859698c07765ff494a 100644 (file)
--- 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 (file)
index 0000000..9f2fff1
--- /dev/null
@@ -0,0 +1,64 @@
+
+/* 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;
+}
diff --git a/parse_args.h b/parse_args.h
new file mode 100644 (file)
index 0000000..3cc51f5
--- /dev/null
@@ -0,0 +1,15 @@
+
+/* parse_args.h
+ *
+ * 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.
+ */
+
+#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 27138d01bd709737ea7b1783836282ca450a049d..2401a389680a0c97d43074731e70c57606c93143 100644 (file)
--- a/totp.c
+++ b/totp.c
 
 /* 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;
 }
diff --git a/totp_args.c b/totp_args.c
deleted file mode 100644 (file)
index 4810a04..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-
-/* 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);
-}
diff --git a/totp_args.h b/totp_args.h
deleted file mode 100644 (file)
index 25a2d53..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-
-/* totp_args.h
- *
- * 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.
- */
-
-#ifndef TOTP_ARGS_H
-#define TOTP_ARGS_H
-
-#include <stdio.h>
-
-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