diff options
author | Thomas Voss <mail@thomasvoss.com> | 2023-08-11 15:22:02 +0200 |
---|---|---|
committer | Thomas Voss <mail@thomasvoss.com> | 2023-08-11 15:22:02 +0200 |
commit | 928ba58c880a3842abdff3c446bfe956c8dc48ae (patch) | |
tree | 8376d9297aecdcfdcae5695f3ff21627cf502263 |
Genesis commit
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile | 16 | ||||
-rw-r--r-- | b32.c | 57 | ||||
-rw-r--r-- | b32.h | 8 | ||||
-rw-r--r-- | main.c | 265 |
5 files changed, 348 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71ca3b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +totp +*.o diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7473a93 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +CC = cc +CFLAGS = -g +LDLIBS = -luriparser -lssl -lcrypto + +all: totp +totp: main.o b32.o + ${CC} ${LDLIBS} -o $@ main.o b32.o + +main.o: main.c b32.h + ${CC} ${CFLAGS} -c main.c + +b32.o: b32.c b32.h + ${CC} ${CFLAGS} -c b32.c + +clean: + rm -f totp *.o @@ -0,0 +1,57 @@ +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +static const uint8_t ctov[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, 0, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +}; + +bool +b32toa(char *dst, const char *src, size_t len) +{ + char c; + int pad = 0; + uint8_t vs[8]; + + while (src[len - 1 - pad] == '=') { + if (++pad > 6) + return false; + } + + for (size_t i = 0; i < len; i += 8) { + for (size_t j = 0; j < 8; j++) { + c = src[i + j]; + vs[j] = ctov[(uint8_t)c]; + if (vs[j] == 255) { + if (c == '=' && j >= 8 - pad) { + vs[j] = 0; + } else { + return false; + } + } + } + + dst[i * 5 / 8 + 0] = (vs[0] << 3) | (vs[1] >> 2); + dst[i * 5 / 8 + 1] = (vs[1] << 6) | (vs[2] << 1) | (vs[3] >> 4); + dst[i * 5 / 8 + 2] = (vs[3] << 4) | (vs[4] >> 1); + dst[i * 5 / 8 + 3] = (vs[4] << 7) | (vs[5] << 2) | (vs[6] >> 3); + dst[i * 5 / 8 + 4] = (vs[6] << 5) | vs[7]; + } + + return true; +} @@ -0,0 +1,8 @@ +#ifndef B32_B32_H +#define B32_B32_H + +#include <stdbool.h> + +bool b32toa(char *, const char *, size_t); + +#endif @@ -0,0 +1,265 @@ +/* References: https://datatracker.ietf.org/doc/html/rfc4226#section-5 */ + +#include <err.h> +#include <getopt.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <time.h> + +#include <openssl/hmac.h> +#include <uriparser/Uri.h> +#include <uriparser/UriBase.h> + +#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; +} |