aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Voss <mail@thomasvoss.com> 2023-08-11 15:22:02 +0200
committerThomas Voss <mail@thomasvoss.com> 2023-08-11 15:22:02 +0200
commit928ba58c880a3842abdff3c446bfe956c8dc48ae (patch)
tree8376d9297aecdcfdcae5695f3ff21627cf502263
Genesis commit
-rw-r--r--.gitignore2
-rw-r--r--Makefile16
-rw-r--r--b32.c57
-rw-r--r--b32.h8
-rw-r--r--main.c265
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
diff --git a/b32.c b/b32.c
new file mode 100644
index 0000000..84810e3
--- /dev/null
+++ b/b32.c
@@ -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;
+}
diff --git a/b32.h b/b32.h
new file mode 100644
index 0000000..090b39f
--- /dev/null
+++ b/b32.h
@@ -0,0 +1,8 @@
+#ifndef B32_B32_H
+#define B32_B32_H
+
+#include <stdbool.h>
+
+bool b32toa(char *, const char *, size_t);
+
+#endif
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 <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;
+}