/* totp.c
*
- * Copyright (c) 2017, mar77i <mar77i at protonmail dot ch>
+ * Copyright (c) 2017-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 <inttypes.h>
#include <limits.h>
#include <stdint.h>
-#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <openssl/hmac.h>
-extern char **environ;
+#include "totp_args.h"
#define INTERVAL_SECONDS 30
-char *find_env_var(const char *match) {
- size_t len = strlen(match);
- char **env;
- if(match == NULL)
- return NULL;
- for(env = environ; *env != NULL; env++)
- if(strncmp(*env, match, len) == 0 && (*env)[len] == '=')
- return *env + len + 1;
- return NULL;
+int b32_decode(char *buf, size_t *len) {
+ const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+ size_t i = 0, used = 0;
+ uint8_t x;
+ char *ptr, *output = buf;
+ for(;;) {
+ while(isspace(buf[i]) || buf[i] == '\n')
+ i++;
+ if(buf[i] == '\0' || buf[i] == '=')
+ break;
+ ptr = strchr(alphabet, toupper(buf[i]));
+ if(ptr == NULL || *ptr == '\0') {
+ fprintf(
+ stderr,
+ "Error: invalid input: '%c' (%02X)\n",
+ buf[i],
+ (uint8_t)buf[i]
+ );
+ return -1;
+ }
+ i++;
+ x = ptr - alphabet;
+ switch(used++ % 8) {
+ case 0:
+ *output = x << 3;
+ break;
+ case 1:
+ *output |= x >> 2;
+ output++;
+ *output = x << 6;
+ break;
+ case 2:
+ *output |= x << 1;
+ break;
+ case 3:
+ *output |= x >> 4;
+ output++;
+ *output = x << 4;
+ break;
+ case 4:
+ *output |= x >> 1;
+ output++;
+ *output = x << 7;
+ break;
+ case 5:
+ *output |= x << 2;
+ break;
+ case 6:
+ *output |= x >> 3;
+ output++;
+ *output = x << 5;
+ break;
+ case 7:
+ *output |= x;
+ output++;
+ break;
+ }
+ }
+ if(len != NULL)
+ *len = output - buf;
+ return 0;
}
-int concat(char *dest, size_t *dest_used, size_t dest_size, char *src) {
- size_t len = strlen(src);
- if(dest == NULL || dest_used == NULL || src == NULL)
- return -1;
- if(*dest_used + len + (*dest_used == 0) > dest_size)
- return -1;
- if(*dest_used == 0)
- (*dest_used)++;
- memcpy(dest + *dest_used - 1, src, len + 1);
- *dest_used += len;
- return 0;
+static inline uint8_t *pack_be64(
+ 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)) & 0xff;
+ return data;
}
-int b32_decode(unsigned char *buf, size_t *len) {
- const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
- size_t i = 0, used = 0;
- unsigned char x, *output = buf;
- char *ptr;
- for(;;) {
- while(isspace(buf[i]) || buf[i] == '\n')
- i++;
- if(buf[i] == '\0' || buf[i] == '=')
- break;
- ptr = strchr(alphabet, toupper(buf[i++]));
- if(ptr == NULL || *ptr == '\0')
- return -1;
- x = ptr - alphabet;
- switch(used++ % 8) {
- case 0:
- *output = x << 3;
- break;
- case 1:
- *output |= x >> 2;
- output++;
- *output = x << 6;
- break;
- case 2:
- *output |= x << 1;
- break;
- case 3:
- *output |= x >> 4;
- output++;
- *output = x << 4;
- break;
- case 4:
- *output |= x >> 1;
- output++;
- *output = x << 7;
- break;
- case 5:
- *output |= x << 2;
- break;
- case 6:
- *output |= x >> 3;
- output++;
- *output = x << 5;
- break;
- case 7:
- *output |= x;
- output++;
- break;
- }
- }
- if(len != NULL)
- *len = output - buf;
- return 0;
-}
-
-static inline unsigned char *pack_be64(unsigned char data[],
- uint64_t value) {
- size_t i;
- for(i = 0; i < sizeof value; i++)
- data[i] = (value >> ((sizeof value - 1 - i) * CHAR_BIT)) & 0xff;
- return data;
-}
-
-int calculate_totp(const char *line) {
- size_t len = 0;
- unsigned int hmac_len = 0;
- unsigned char data[sizeof(uint64_t)], *ptr, i;
- if(line == NULL)
- return -1;
- if(strlen(line) < 2)
- return 0;
- ptr = (unsigned char*)strchr(line, ':');
- if(ptr == NULL)
- return -1;
- printf("%.*s: ", (int)((char*)ptr - line), line);
- ptr++;
- if(b32_decode(ptr, &len) < 0) {
- fprintf(stderr, "Error: b32_decode.\n");
- return -1;
- }
+int calculate_totp(const char *line, const char *search) {
+ size_t len = 0;
+ unsigned int hmac_len = 0, label_len;
+ uint8_t data[sizeof(uint64_t)], i;
+ char *ptr, *find;
+ if(line == NULL)
+ return -1;
+ if(strlen(line) < 2)
+ return 0;
+ ptr = strrchr(line, ':');
+ if(ptr == NULL)
+ return -1;
+ label_len = ptr - line;
+ if (search != NULL) {
+ find = strcasestr(line, search);
+ if (find == NULL || find + strlen(search) - line > label_len)
+ return 0;
+ }
+ ptr++;
+ if(b32_decode(ptr, &len) < 0) {
+ fprintf(stderr, "Error: b32_decode.\n");
+ return -1;
+ }
- // fill data with the current time divided by INTERVAL_SECONDS
- pack_be64(data, time(NULL) / INTERVAL_SECONDS);
- if(HMAC(EVP_sha1(), ptr, len, data, sizeof data,
- ptr, &hmac_len) == NULL) {
- fprintf(stderr, "\nError: hmac.\n");
- return -1;
- }
- i = ptr[hmac_len - 1] & 0xf;
- printf("%06"PRIu32"\n", (
- ((ptr[i] & 0x7f) << 24) | (ptr[i + 1] << 16) |
- (ptr[i + 2] << 8) | ptr[i + 3]
- ) % 1000000);
- return 0;
+ // fill data with the current time divided by INTERVAL_SECONDS
+ pack_be64(data, time(NULL) / INTERVAL_SECONDS);
+ if(
+ HMAC(
+ EVP_sha1(),
+ ptr,
+ len,
+ data,
+ sizeof data,
+ (uint8_t*)ptr,
+ &hmac_len
+ ) == NULL
+ ) {
+ fprintf(stderr, "\nError: hmac.\n");
+ return -1;
+ }
+ i = ptr[hmac_len - 1] & 0xf;
+ printf(
+ "%.*s: %06"PRIu32"\n",
+ label_len,
+ line,
+ (
+ (((uint8_t)ptr[i] & 0x7f) << 24) | ((uint8_t)ptr[i + 1] << 16) |
+ ((uint8_t)ptr[i + 2] << 8) | (uint8_t)ptr[i + 3]
+ ) % 1000000
+ );
+ return 0;
}
-int main(void) {
- FILE *fh;
- size_t buf_used = 0;
- int ret = EXIT_FAILURE;
- char buf[PATH_MAX], *ptr = find_env_var("HOME");
- if(ptr == NULL)
- return EXIT_FAILURE;
- if(concat(buf, &buf_used, sizeof buf, ptr) < 0)
- return EXIT_FAILURE;
- ptr = buf + buf_used - 2;
- while(*ptr == '/') {
- *ptr-- = '\0';
- buf_used--;
- }
- if(concat(buf, &buf_used, sizeof buf, "/.otp_secrets") < 0)
- return EXIT_FAILURE;
- fh = fopen(buf, "rb");
- if(fh == NULL) {
- perror("fopen");
- return EXIT_FAILURE;
- }
- while(fgets(buf, sizeof buf, fh) != NULL)
- if(calculate_totp(buf) < 0)
- goto error;
- if(ferror(fh)) {
- fprintf(stderr, "Error: read error.");
- goto error;
- }
- ret = EXIT_SUCCESS;
+int main(int argc, char *argv[]) {
+ struct totp_args ta = TOTP_ARGS_INIT(argc, argv);
+ int ret = EXIT_FAILURE;
+ char buf[PATH_MAX];
+ switch (totp_args_setup(&ta)) {
+ case 1:
+ ret = EXIT_SUCCESS;
+ // FALLTHROUGH
+ case -1:
+ 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");
+ goto error;
+ }
+ ret = EXIT_SUCCESS;
error:
- fclose(fh);
- return ret;
+ totp_args_cleanup(ta);
+ 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);
+}