From 928ba58c880a3842abdff3c446bfe956c8dc48ae Mon Sep 17 00:00:00 2001 From: Thomas Voss Date: Fri, 11 Aug 2023 15:22:02 +0200 Subject: Genesis commit --- main.c | 265 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 main.c (limited to 'main.c') diff --git a/main.c b/main.c new file mode 100644 index 0000000..d6168ab --- /dev/null +++ b/main.c @@ -0,0 +1,265 @@ +/* References: https://datatracker.ietf.org/doc/html/rfc4226#section-5 */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "b32.h" + +#define DIE(...) err(EXIT_FAILURE, __VA_ARGS__) +#define DIEX(...) errx(EXIT_FAILURE, __VA_ARGS__) +#define STREQ(x, y) (strcmp(x, y) == 0) +#define PRINT_CODE(w, x) printf("%0*d\n", (int)w, x) + +#define TOTP_DEFAULT (struct totp_config){ .len = 6, .p = 30 } + +typedef unsigned char uchar; + +struct totp_config { + const char *enc_sec; + long len, p; +}; + +extern char *__progname; + +static const char *bad_scheme = "Invalid scheme ‘%.*s’; expected ‘otpauth’"; +static const char *bad_param = "Invalid ‘%s’ parameter provided"; +static const char *empty_param = "Empty ‘%s’ parameter provided"; +static const char *usage_s = + "Usage: %s [-d digits] [-p period] [secret ...]\n" + " %s [-u] [uri ...]\n"; + +static void usage(void); +static bool strtol_safe(long *, const char *); +static bool totp(struct totp_config, uint32_t *); +static uint32_t pow32(uint32_t, uint32_t); +static bool uri_parse(struct totp_config *, const char *); + +void +usage(void) +{ + fprintf(stderr, usage_s, __progname, __progname); + exit(EXIT_FAILURE); +} + +int +main(int argc, char *argv[]) +{ + int opt, rv; + bool uflag = false; + long n; + char *buf; + size_t bufsiz; + ssize_t nr; + uint32_t code; + struct totp_config conf = TOTP_DEFAULT; + struct option longopts[] = { + {"digits", required_argument, 0, 'd'}, + {"period", required_argument, 0, 'p'}, + {"uri", no_argument, 0, 'u'}, + { NULL, 0, 0, 0 }, + }; + + while ((opt = getopt_long(argc, argv, "d:p:u", longopts, NULL)) != -1) { + switch (opt) { + case 'd': + case 'p': + if (!strtol_safe(&n, optarg)) + DIEX(bad_param, opt == 'd' ? "digits" : "period"); + if (opt == 'd') + conf.len = n; + else + conf.p = n; + break; + case 'u': + uflag = true; + break; + default: + usage(); + } + } + + argc -= optind; + argv += optind; + + rv = EXIT_SUCCESS; + + for (int i = 0; i < argc; i++) { + conf.enc_sec = argv[i]; + if (uflag) { + conf = TOTP_DEFAULT; + if (!uri_parse(&conf, argv[i])) { + rv = EXIT_FAILURE; + continue; + } + } else + conf.enc_sec = argv[i]; + if (totp(conf, &code)) + PRINT_CODE(conf.len, code); + } + + if (argc == 0) { + buf = NULL; + bufsiz = 0; + + while ((nr = getline(&buf, &bufsiz, stdin)) > 0) { + if (buf[--nr] == '\n') + buf[nr] = '\0'; + if (uflag) { + conf = TOTP_DEFAULT; + if (!uri_parse(&conf, buf)) { + rv = EXIT_FAILURE; + continue; + } + } else + conf.enc_sec = buf; + if (totp(conf, &code)) + PRINT_CODE(conf.len, code); + } + } + + return rv; +} + +bool +uri_parse(struct totp_config *conf, const char *uri_raw) +{ + int n; + bool reject; + size_t len; + UriUriA uri; + UriQueryListA *qs; + const char *epos; + + if ((n = uriParseSingleUriA(&uri, uri_raw, &epos)) != URI_SUCCESS) { + len = epos - uri_raw; + warnx("Failed to parse URI ‘%s’\n" + "%*c Error detected here", + uri_raw, (int)(len + 24 + strlen(__progname)), '^'); + return false; + } + + len = uri.scheme.afterLast - uri.scheme.first; + reject = len != strlen("otpauth"); + reject = reject || strncasecmp(uri.scheme.first, "otpauth", len) != 0; + + if (reject) { + warnx(bad_scheme, (int)len, uri.scheme.first); + return false; + } + if (uriDissectQueryMallocA(&qs, NULL, uri.query.first, + uri.query.afterLast) != URI_SUCCESS) { + warnx("Failed to parse query string"); + return false; + } + + for (UriQueryListA *p = qs; p != NULL; p = p->next) { + if (STREQ(p->key, "secret")) { + if (p->value == NULL) { + warnx("Secret key has no value"); + return false; + } + conf->enc_sec = p->value; + } else if (STREQ(p->key, "digits")) { + if (p->value == NULL) { + warnx(empty_param, "digits"); + return false; + } + if (!strtol_safe(&conf->len, p->value)) { + warnx(bad_param, "digits"); + return false; + } + } else if (STREQ(p->key, "period")) { + if (p->value == NULL) { + warnx(empty_param, "period"); + return false; + } + if (!strtol_safe(&conf->p, p->value)) { + warnx(bad_param, "period"); + return false; + } + } + } + + uriFreeQueryListA(qs); + uriFreeUriMembersA(&uri); + + return true; +} + +bool +totp(struct totp_config conf, uint32_t *code) +{ + int off; + char *key; + uchar *mac; + time_t epoch; + size_t keylen; + uint8_t buf[sizeof(time_t)]; /* Enough for a 64bit num */ + uint32_t binc; + + /* TODO: conf.enc_sec needs to be ‘=’ padded to a multiple of 8 */ + + /* When decoding base32, you need ceil(conf.enc_sec / 1.6) bytes */ + keylen = strlen(conf.enc_sec) / 1.6 + 1; + key = calloc(keylen, sizeof(char)); + b32toa(key, conf.enc_sec, strlen(conf.enc_sec)); + + if (time(&epoch) == (time_t)-1) + DIE("time"); + + epoch /= conf.p; + + buf[0] = (epoch >> 56) & 0xFF; + buf[1] = (epoch >> 48) & 0xFF; + buf[2] = (epoch >> 40) & 0xFF; + buf[3] = (epoch >> 32) & 0xFF; + buf[4] = (epoch >> 24) & 0xFF; + buf[5] = (epoch >> 16) & 0xFF; + buf[6] = (epoch >> 8) & 0xFF; + buf[7] = (epoch >> 0) & 0xFF; + + mac = HMAC(EVP_sha1(), key, keylen, buf, sizeof(buf), NULL, NULL); + if (mac == NULL) + return false; + + /* SHA1 hashes are 20 bytes long */ + off = mac[19] & 0x0F; + binc = (mac[off + 0] & 0x7F) << 24 + | (mac[off + 1] & 0xFF) << 16 + | (mac[off + 2] & 0xFF) << 8 + | (mac[off + 3] & 0xFF) << 0; + + *code = binc % pow32(10, conf.len); + return true; +} + +bool +strtol_safe(long *n, const char *s) +{ + char *e; + *n = strtol(s, &e, 10); + return *n > 0 && *s != '\0' && *e == '\0'; +} + +/* This could overflow if you did some autistic shit */ +uint32_t +pow32(uint32_t x, uint32_t y) +{ + int n = x; + if (y == 0) + return 1; + while (--y != 0) + x *= n; + return x; +} -- cgit v1.2.3