aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Makefile28
-rw-r--r--b32.c61
-rw-r--r--b32.h10
-rw-r--r--cbs.h638
-rw-r--r--main.c273
-rw-r--r--make.c226
-rw-r--r--src/base32.c93
-rw-r--r--src/base32.h10
-rw-r--r--src/common.h27
-rw-r--r--src/hmac.c41
-rw-r--r--src/hmac.h11
-rw-r--r--src/main.c191
-rw-r--r--src/sha1-generic.c78
-rw-r--r--src/sha1-x64.c98
-rw-r--r--src/sha1.c76
-rw-r--r--src/sha1.h21
-rw-r--r--src/xendian.h21
18 files changed, 1533 insertions, 372 deletions
diff --git a/.gitignore b/.gitignore
index 71ca3b7..176300b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
totp
+totp-x64
+make
*.o
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 03f01c4..0000000
--- a/Makefile
+++ /dev/null
@@ -1,28 +0,0 @@
-.POSIX:
-
-CC = cc
-CFLAGS = \
- -Wall -Wextra -pedantic -Wshadow -Wpointer-arith -Wcast-align \
- -Wwrite-strings -Wmissing-prototypes -Wmissing-declarations \
- -Wredundant-decls -Wnested-externs -Winline -Wno-long-long \
- -Wconversion -Wstrict-prototypes \
- -O3 -march=native -mtune=native -pipe
-LDLIBS = -luriparser -lssl -lcrypto
-
-PREFIX = /usr/local
-DPREFIX = ${DESTDIR}${PREFIX}
-
-all: totp
-totp: main.o b32.o
- ${CC} ${LDLIBS} -o $@ main.o b32.o
-
-main.o: main.c b32.h
-b32.o: b32.c b32.h
-
-install:
- mkdir -p ${DPREFIX}/bin ${DPREFIX}/share/man/man1
- cp totp ${DPREFIX}/bin
- cp totp.1 ${DPREFIX}/share/man/man1
-
-clean:
- rm -f totp *.o
diff --git a/b32.c b/b32.c
deleted file mode 100644
index be8532f..0000000
--- a/b32.c
+++ /dev/null
@@ -1,61 +0,0 @@
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-#include "b32.h"
-
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wsign-conversion"
-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,
-};
-#pragma GCC diagnostic pop
-
-bool
-b32toa(uint8_t *dst, const char *src, size_t len)
-{
- char c;
- size_t 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] == (uint8_t)-1) {
- 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] >> 0);
- }
-
- return true;
-}
diff --git a/b32.h b/b32.h
deleted file mode 100644
index d3c0f55..0000000
--- a/b32.h
+++ /dev/null
@@ -1,10 +0,0 @@
-#ifndef B32_B32_H
-#define B32_B32_H
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-bool b32toa(uint8_t *, const char *, size_t);
-
-#endif
diff --git a/cbs.h b/cbs.h
new file mode 100644
index 0000000..0f8551c
--- /dev/null
+++ b/cbs.h
@@ -0,0 +1,638 @@
+/*
+ * BSD Zero Clause License
+ *
+ * Copyright © 2023–2024 Thomas Voss
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+ * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ * PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef C_BUILD_SYSTEM_H
+#define C_BUILD_SYSTEM_H
+
+#define _GNU_SOURCE
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <assert.h>
+#include <errno.h>
+#include <limits.h>
+#ifndef CBS_NO_THREADS
+# include <pthread.h>
+#endif
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <wordexp.h>
+
+#define _vtoxs(...) ((char *[]){__VA_ARGS__})
+
+#define lengthof(xs) (sizeof(xs) / sizeof(*(xs)))
+
+struct strs {
+ char **buf;
+ size_t len, cap;
+};
+
+enum pkg_config_flags {
+ PC_CFLAGS = 1 << 0,
+ PC_LIBS = 1 << 1,
+ PC_SHARED = 1 << 2,
+ PC_STATIC = 1 << 3,
+};
+
+static void cbsinit(int, char **);
+static void rebuild(const char *); /* Always call via macro wrapper */
+#define rebuild() rebuild(__FILE__)
+
+static void strsfree(struct strs *);
+static void strszero(struct strs *);
+static void strspush(struct strs *, char **, size_t);
+static void strspushenv(struct strs *, const char *, char **, size_t);
+#define strspushl(xs, ...) \
+ strspush((xs), _vtoxs(__VA_ARGS__), lengthof(_vtoxs(__VA_ARGS__)))
+#define strspushenvl(xs, ev, ...) \
+ strspushenv((xs), (ev), _vtoxs(__VA_ARGS__), lengthof(_vtoxs(__VA_ARGS__)))
+
+static bool fexists(const char *);
+static int fmdcmp(const char *, const char *);
+static bool fmdolder(const char *, const char *);
+static bool fmdnewer(const char *, const char *);
+static bool foutdated(const char *, char **, size_t);
+#define foutdatedl(s, ...) \
+ foutdated((s), _vtoxs(__VA_ARGS__), lengthof(_vtoxs(__VA_ARGS__)))
+
+static int cmdexec(struct strs);
+static pid_t cmdexec_async(struct strs);
+static int cmdexec_read(struct strs, char **, size_t *);
+static int cmdwait(pid_t);
+static void cmdput(struct strs);
+static void cmdfput(FILE *, struct strs);
+
+static char *swpext(const char *, const char *);
+static bool pcquery(struct strs *, const char *, int);
+static bool binexists(const char *);
+static int nproc(void);
+
+#ifndef CBS_NO_THREADS
+typedef void tjob(void *);
+typedef void tjob_free(void *);
+
+struct _tqueue {
+ void *arg;
+ tjob *fn;
+ tjob_free *free;
+ struct _tqueue *next;
+};
+
+typedef struct {
+ bool stop;
+ size_t tcnt, left;
+ pthread_t *thrds;
+ pthread_cond_t cnd;
+ pthread_mutex_t mtx;
+ struct _tqueue *head, *tail;
+} tpool;
+
+static void tpinit(tpool *, size_t);
+static void tpfree(tpool *);
+static void tpwait(tpool *);
+static void tpenq(tpool *, tjob *, void *, tjob_free *);
+#endif /* !CBS_NO_THREADS */
+
+static int _cbs_argc;
+static char **_cbs_argv;
+
+/* Implementation */
+
+#ifdef __GNUC__
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wunused-function"
+#endif
+
+#ifdef __APPLE__
+# define st_mtim st_mtimespec
+#endif
+
+void
+cbsinit(int argc, char **argv)
+{
+ _cbs_argc = argc;
+ _cbs_argv = malloc(sizeof(char *) * (argc + 1));
+ assert(_cbs_argv != NULL);
+ for (int i = 0; i < argc; i++) {
+ _cbs_argv[i] = strdup(argv[i]);
+ assert(_cbs_argv[i] != NULL);
+ }
+ _cbs_argv[argc] = NULL;
+
+ char *s = strrchr(_cbs_argv[0], '/');
+ if (s != NULL) {
+ s[0] = 0;
+ assert(chdir(_cbs_argv[0]) != -1);
+ s[0] = '/';
+ }
+}
+
+void
+(rebuild)(const char *path)
+{
+ char *src, *dst;
+
+ if ((src = strrchr(path, '/')) != NULL)
+ src++;
+ else
+ src = (char *)path;
+
+ if ((dst = strrchr(*_cbs_argv, '/')) != NULL)
+ dst++;
+ else
+ dst = *_cbs_argv;
+
+ if (!foutdatedl(dst, src))
+ return;
+
+ struct strs xs = {0};
+ strspushenvl(&xs, "CC", "cc");
+#ifndef CBS_NO_THREADS
+ strspushl(&xs, "-lpthread");
+#endif
+ strspushl(&xs, "-o", dst, src);
+ cmdput(xs);
+ assert(cmdexec(xs) == EXIT_SUCCESS);
+
+ execvp(*_cbs_argv, _cbs_argv);
+ assert(!"failed to execute process");
+}
+
+void
+strsfree(struct strs *xs)
+{
+ free(xs->buf);
+ xs->buf = NULL;
+ xs->len = xs->cap = 0;
+}
+
+void
+strszero(struct strs *xs)
+{
+ xs->len = 0;
+ if (xs->cap > 0)
+ xs->buf[0] = NULL;
+}
+
+void
+strspush(struct strs *xs, char **ys, size_t n)
+{
+ if (n == 0)
+ return;
+
+ if (xs->len + n >= xs->cap) {
+ xs->cap = (xs->len + n) * 2;
+ xs->buf = realloc(xs->buf, sizeof(char *) * (xs->cap + 1));
+ assert(xs->buf != NULL);
+ }
+ memcpy(xs->buf + xs->len, ys, n * sizeof(char *));
+ xs->len += n;
+
+ assert(xs->len <= xs->cap);
+ xs->buf[xs->len] = NULL;
+}
+
+void
+strspushenv(struct strs *xs, const char *ev, char **ys, size_t n)
+{
+ /* NOTE: Do your best to NOT modify any pushed envvar! */
+ char *p = getenv(ev);
+ if (p == NULL || *p == 0) {
+ strspush(xs, ys, n);
+ return;
+ }
+
+ wordexp_t we;
+ assert(wordexp(p, &we, WRDE_NOCMD) == 0);
+
+ /* TODO: Memory leak! */
+ for (size_t i = 0; i < we.we_wordc; i++) {
+ char *w = strdup(we.we_wordv[i]);
+ assert(w != NULL);
+ strspushl(xs, w);
+ }
+
+ wordfree(&we);
+}
+
+bool
+fexists(const char *f)
+{
+ return !access(f, F_OK);
+}
+
+int
+fmdcmp(const char *lhs, const char *rhs)
+{
+ int errnol, errnor;
+ struct stat sbl, sbr;
+
+ stat(lhs, &sbl); errnol = errno; errno = 0;
+ stat(rhs, &sbr); errnor = errno;
+
+ assert(errnol == 0 || errnol == ENOENT);
+ assert(errnor == 0 || errnor == ENOENT);
+
+ if (errnol == ENOENT && errnor == ENOENT)
+ return 0;
+ if (errnol == ENOENT)
+ return -1;
+ if (errnor == ENOENT)
+ return +1;
+
+ return sbl.st_mtim.tv_sec == sbr.st_mtim.tv_sec
+ ? sbl.st_mtim.tv_nsec - sbr.st_mtim.tv_nsec
+ : sbl.st_mtim.tv_sec - sbr.st_mtim.tv_sec;
+}
+
+bool
+fmdnewer(const char *lhs, const char *rhs)
+{
+ return fmdcmp(lhs, rhs) > 0;
+}
+
+bool
+fmdolder(const char *lhs, const char *rhs)
+{
+ return fmdcmp(lhs, rhs) < 0;
+}
+
+bool
+foutdated(const char *src, char **deps, size_t n)
+{
+ for (size_t i = 0; i < n; i++) {
+ if (fmdolder(src, deps[i]))
+ return true;
+ }
+ return false;
+}
+
+int
+cmdexec(struct strs xs)
+{
+ return cmdwait(cmdexec_async(xs));
+}
+
+pid_t
+cmdexec_async(struct strs xs)
+{
+ pid_t pid = fork();
+ assert(pid != -1);
+ if (pid == 0) {
+ execvp(xs.buf[0], xs.buf);
+ assert(!"failed to execute process");
+ }
+ return pid;
+}
+
+int
+cmdexec_read(struct strs xs, char **p, size_t *n)
+{
+ enum {
+ R,
+ W,
+ };
+ int fds[2];
+
+ assert(pipe(fds) != -1);
+
+ pid_t pid = fork();
+ assert(pid != -1);
+
+ if (pid == 0) {
+ close(fds[R]);
+ close(STDOUT_FILENO);
+ assert(dup2(fds[W], STDOUT_FILENO) != -1);
+ execvp(xs.buf[0], xs.buf);
+ assert(!"failed to execute process");
+ }
+
+ close(fds[W]);
+
+ struct stat sb;
+ assert(fstat(fds[R], &sb) != -1);
+
+ *p = NULL, *n = 0;
+ char *buf = malloc(sb.st_blksize);
+ assert(buf != NULL);
+
+ for (;;) {
+ ssize_t nr;
+ if ((nr = read(fds[R], buf, sb.st_blksize)) == 0)
+ break;
+ assert(nr != -1);
+
+ *p = realloc(*p, *n + nr + 1);
+ assert(*p != NULL);
+
+ memcpy(*p + *n, buf, nr);
+ *n += nr;
+ }
+
+ close(fds[R]);
+ free(buf);
+
+ return cmdwait(pid);
+}
+
+int
+cmdwait(pid_t pid)
+{
+ int ws;
+ assert(waitpid(pid, &ws, 0) != -1);
+ if (WIFEXITED(ws))
+ return WEXITSTATUS(ws);
+ return WIFEXITED(ws) ? WEXITSTATUS(ws) : 256;
+}
+
+/*
+ * import shlex
+ *
+ * s = '#define _SHELL_SAFE "'
+ * for c in map(chr, range(128)):
+ * if not shlex._find_unsafe(c):
+ * s += c
+ * print(s + '"')
+ */
+#define _SHELL_SAFE \
+ "%+,-./0123456789:=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"
+
+void
+cmdput(struct strs xs)
+{
+ cmdfput(stdout, xs);
+}
+
+void
+cmdfput(FILE *fp, struct strs xs)
+{
+ flockfile(fp);
+ for (size_t i = 0; i < xs.len; i++) {
+ bool safe = true;
+ const char *p = xs.buf[i];
+
+ for (const char *q = p; *q; q++) {
+ if (!strchr(_SHELL_SAFE, *q)) {
+ safe = false;
+ break;
+ }
+ }
+
+ if (safe)
+ fputs(p, fp);
+ else {
+ putc('\'', fp);
+ for (const char *q = p; *q; q++) {
+ if (*q == '\'')
+ fputs("'\"'\"'", fp);
+ else
+ putc(*q, fp);
+ }
+ putc('\'', fp);
+ }
+
+ putc(i == xs.len - 1 ? '\n' : ' ', fp);
+ }
+ funlockfile(fp);
+}
+
+bool
+pcquery(struct strs *xs, const char *lib, int flags)
+{
+ struct strs ys = {0};
+
+ strspushl(&ys, "pkg-config", "--silence-errors");
+ if (flags & PC_CFLAGS)
+ strspushl(&ys, "--cflags");
+ if (flags & PC_LIBS)
+ strspushl(&ys, "--libs");
+ if (flags & PC_SHARED)
+ strspushl(&ys, "--shared");
+ if (flags & PC_STATIC)
+ strspushl(&ys, "--static");
+ strspushl(&ys, (char *)lib);
+
+ char *buf;
+ size_t bufsz;
+ int ec = cmdexec_read(ys, &buf, &bufsz);
+ strsfree(&ys);
+ if (ec != EXIT_SUCCESS)
+ return false;
+
+ /* Remove trailing newline */
+ buf[bufsz - 1] = 0;
+
+ wordexp_t we;
+ assert(wordexp(buf, &we, WRDE_NOCMD) == 0);
+
+ char **words = malloc(sizeof(char *) * we.we_wordc);
+ assert(words != NULL);
+
+ /* TODO: Memory leak! */
+ for (size_t i = 0; i < we.we_wordc; i++)
+ assert((words[i] = strdup(we.we_wordv[i])) != NULL);
+
+ strspush(xs, words, we.we_wordc);
+ wordfree(&we);
+ free(words);
+ free(buf);
+ return true;
+}
+
+bool
+binexists(const char *s)
+{
+ const char *path = getenv("PATH");
+ assert(path != NULL);
+
+ char *p = strdup(path), *it;
+ assert(p != NULL);
+
+ for (it = strtok(p, ":"); it != NULL; it = strtok(NULL, ":")) {
+ static char buf[PATH_MAX];
+ memset(buf, 0, sizeof(buf));
+ snprintf(buf, sizeof(buf), "%s/%s", it, s);
+ if (fexists(buf)) {
+ free(p);
+ return true;
+ }
+ }
+
+ free(p);
+ return false;
+}
+
+int
+nproc(void)
+{
+#ifdef _SC_NPROCESSORS_ONLN
+ return (int)sysconf(_SC_NPROCESSORS_ONLN);
+#else
+ errno = 0;
+ return -1;
+#endif
+}
+
+char *
+swpext(const char *file, const char *ext)
+{
+ const char *p = strrchr(file, '.');
+ if (p == NULL) {
+ p = strdup(file);
+ assert(p != NULL);
+ return (char *)p;
+ }
+
+ size_t noextlen = p - file;
+ char *s = malloc(noextlen + strlen(ext) + 2);
+ assert(s != NULL);
+ sprintf(s, "%.*s.%s", (int)noextlen, file, ext);
+ return s;
+}
+
+#ifndef CBS_NO_THREADS
+static struct _tqueue *
+_tpdeq(tpool *tp)
+{
+ struct _tqueue *q = tp->head;
+
+ if (q != NULL) {
+ tp->head = tp->head->next;
+ if (!tp->head)
+ tp->tail = NULL;
+ }
+
+ return q;
+}
+
+static void *
+_tpwork(void *arg)
+{
+ tpool *tp = arg;
+
+ while (!tp->stop) {
+ struct _tqueue *q;
+
+ pthread_mutex_lock(&tp->mtx);
+ while (!tp->stop && !tp->head)
+ pthread_cond_wait(&tp->cnd, &tp->mtx);
+ if (tp->stop) {
+ pthread_mutex_unlock(&tp->mtx);
+ break;
+ }
+
+ q = _tpdeq(tp);
+ pthread_mutex_unlock(&tp->mtx);
+
+ q->fn(q->arg);
+ if (q->free)
+ q->free(q->arg);
+ free(q);
+
+ pthread_mutex_lock(&tp->mtx);
+ tp->left--;
+ pthread_cond_broadcast(&tp->cnd);
+ pthread_mutex_unlock(&tp->mtx);
+ }
+
+ return NULL;
+}
+
+void
+tpinit(tpool *tp, size_t n)
+{
+ tp->tcnt = n;
+ tp->stop = false;
+ tp->left = 0;
+ tp->head = tp->tail = NULL;
+ tp->thrds = malloc(sizeof(pthread_t) * n);
+ assert(tp->thrds != NULL);
+ pthread_cond_init(&tp->cnd, NULL);
+ pthread_mutex_init(&tp->mtx, NULL);
+ for (size_t i = 0; i < n; i++)
+ assert(pthread_create(tp->thrds + i, NULL, _tpwork, tp) == 0);
+}
+
+void
+tpfree(tpool *tp)
+{
+ tp->stop = true;
+
+ pthread_mutex_lock(&tp->mtx);
+ pthread_cond_broadcast(&tp->cnd);
+ pthread_mutex_unlock(&tp->mtx);
+
+ for (size_t i = 0; i < tp->tcnt; i++)
+ pthread_join(tp->thrds[i], NULL);
+
+ free(tp->thrds);
+ while (tp->head != NULL) {
+ struct _tqueue *q = _tpdeq(tp);
+ if (q->free)
+ q->free(q->arg);
+ free(q);
+ }
+
+ pthread_cond_destroy(&tp->cnd);
+ pthread_mutex_destroy(&tp->mtx);
+}
+
+void
+tpwait(tpool *tp)
+{
+ pthread_mutex_lock(&tp->mtx);
+ while (!tp->stop && tp->left)
+ pthread_cond_wait(&tp->cnd, &tp->mtx);
+ pthread_mutex_unlock(&tp->mtx);
+}
+
+void
+tpenq(tpool *tp, tjob *fn, void *arg, tjob_free *free)
+{
+ struct _tqueue *q = malloc(sizeof(*q));
+ assert(q != NULL);
+ *q = (struct _tqueue){
+ .fn = fn,
+ .arg = arg,
+ .free = free,
+ };
+
+ pthread_mutex_lock(&tp->mtx);
+ if (tp->tail)
+ tp->tail->next = q;
+ if (!tp->head)
+ tp->head = q;
+ tp->tail = q;
+ tp->left++;
+ pthread_cond_signal(&tp->cnd);
+ pthread_mutex_unlock(&tp->mtx);
+}
+#endif /* !CBS_NO_THREADS */
+
+#ifdef __GNUC__
+# pragma GCC diagnostic pop
+#endif
+
+#ifdef __APPLE__
+# undef st_mtim
+#endif
+
+#endif /* !C_BUILD_SYSTEM_H */
diff --git a/main.c b/main.c
deleted file mode 100644
index 67a4815..0000000
--- a/main.c
+++ /dev/null
@@ -1,273 +0,0 @@
-/* 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 STREQ(x, y) (strcmp(x, y) == 0)
-#define WARNX_AND_RET(...) \
- do { \
- rv = EXIT_FAILURE; \
- warnx(__VA_ARGS__); \
- return false; \
- } while (false)
-
-#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 int rv;
-
-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] [-u] [secret ...]\n";
-
-static void usage(void);
-static void totp_print(struct totp_config, char *, bool);
-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 *);
-static bool big_endian(void);
-
-void
-usage(void)
-{
- fprintf(stderr, usage_s, __progname);
- exit(EXIT_FAILURE);
-}
-
-int
-main(int argc, char *argv[])
-{
- int opt;
- bool uflag = false;
- long n;
- char *buf;
- size_t bufsiz;
- ssize_t nr;
- 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))
- errx(EXIT_FAILURE, 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;
-
- if (argc == 0) {
- buf = NULL;
- bufsiz = 0;
-
- while ((nr = getline(&buf, &bufsiz, stdin)) > 0) {
- if (buf[--nr] == '\n')
- buf[nr] = '\0';
- totp_print(conf, buf, uflag);
- }
- free(buf);
- } else for (int i = 0; i < argc; i++)
- totp_print(conf, argv[i], uflag);
-
- return rv;
-}
-
-void
-totp_print(struct totp_config conf, char *buf, bool uflag)
-{
- uint32_t code;
-
- if (uflag) {
- conf = TOTP_DEFAULT;
- if (!uri_parse(&conf, buf))
- return;
- } else
- conf.enc_sec = buf;
- if (totp(conf, &code))
- printf("%0*d\n", (int)conf.len, code);
- if (uflag)
- free((void *)conf.enc_sec);
-}
-
-bool
-uri_parse(struct totp_config *conf, const char *uri_raw)
-{
- bool reject;
- size_t len;
- UriUriA uri;
- UriQueryListA *qs;
- const char *epos;
-
- if (uriParseSingleUriA(&uri, uri_raw, &epos) != URI_SUCCESS) {
- len = (size_t)(epos - uri_raw) + 24 + strlen(__progname);
- WARNX_AND_RET("Failed to parse URI ‘%s’\n"
- "%*c Error detected here",
- uri_raw, (int)len, '^');
- }
-
- len = (size_t)(uri.scheme.afterLast - uri.scheme.first);
- reject = len != strlen("otpauth");
- reject = reject || strncasecmp(uri.scheme.first, "otpauth", len) != 0;
-
- if (reject)
- WARNX_AND_RET(bad_scheme, (int)len, uri.scheme.first);
- if (uriDissectQueryMallocA(&qs, NULL, uri.query.first,
- uri.query.afterLast) != URI_SUCCESS)
- WARNX_AND_RET("Failed to parse query string");
-
- for (UriQueryListA *p = qs; p != NULL; p = p->next) {
- if (STREQ(p->key, "secret")) {
- if (p->value == NULL)
- WARNX_AND_RET("Secret key has no value");
- if ((conf->enc_sec = strdup(p->value)) == NULL)
- err(EXIT_FAILURE, "strdup");
- } else if (STREQ(p->key, "digits")) {
- if (p->value == NULL)
- WARNX_AND_RET(empty_param, "digits");
- if (!strtol_safe(&conf->len, p->value))
- WARNX_AND_RET(bad_param, "digits");
- } else if (STREQ(p->key, "period")) {
- if (p->value == NULL)
- WARNX_AND_RET(empty_param, "period");
- if (!strtol_safe(&conf->p, p->value))
- WARNX_AND_RET(bad_param, "period");
- }
- }
-
- uriFreeQueryListA(qs);
- uriFreeUriMembersA(&uri);
-
- return true;
-}
-
-bool
-totp(struct totp_config conf, uint32_t *code)
-{
- int off;
- bool clean;
- uint8_t *key;
- char *enc_sec;
- uchar *mac;
- time_t epoch;
- uint8_t buf[sizeof(time_t)];
- uint32_t binc;
- size_t keylen, enc_sec_len, old;
-
- /* conf.enc_sec needs to be ‘=’ padded to a multiple of 8 */
- old = enc_sec_len = strlen(conf.enc_sec);
- if (enc_sec_len % 8 == 0) {
- enc_sec = (char *)conf.enc_sec;
- clean = false;
- } else {
- enc_sec_len += 8 - enc_sec_len % 8;
- if ((enc_sec = malloc(enc_sec_len)) == NULL)
- err(EXIT_FAILURE, "malloc");
- memcpy(enc_sec, conf.enc_sec, old);
- memset(enc_sec + old, '=', enc_sec_len - old);
- clean = true;
- }
-
- keylen = (size_t)((double)old / 1.6);
- if ((key = calloc(keylen + 1, sizeof(char))) == NULL)
- err(EXIT_FAILURE, "calloc");
- b32toa(key, enc_sec, enc_sec_len);
-
- if (time(&epoch) == (time_t)-1) {
- warn("time");
- return false;
- }
-
- epoch /= conf.p;
-
- if (big_endian())
- memcpy(buf, &epoch, sizeof(time_t));
- else for (size_t i = sizeof(buf); i --> 0;)
- buf[sizeof(buf)-1-i] = (uint8_t)((epoch >> 8 * i) & 0xFF);
-
- mac = HMAC(EVP_sha1(), key, (int)keylen, buf, sizeof(buf), NULL, NULL);
- if (mac == NULL)
- WARNX_AND_RET("Failed to compute HMAC SHA-1 hash");
-
- /* 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, (uint32_t)conf.len);
-
- if (clean)
- free(enc_sec);
- free(key);
- 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)
-{
- uint32_t n = x;
- if (y == 0)
- return 1;
- while (--y != 0)
- x *= n;
- return x;
-}
-
-bool
-big_endian(void)
-{
- unsigned n = 0x01020304;
- uchar *ptr = (uchar *)&n;
-
- return *ptr == 1;
-}
diff --git a/make.c b/make.c
new file mode 100644
index 0000000..82e3b0f
--- /dev/null
+++ b/make.c
@@ -0,0 +1,226 @@
+#include <assert.h>
+#include <getopt.h>
+#include <glob.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define CBS_NO_THREADS
+#include "cbs.h"
+
+static void cc(void *);
+static void ld(void);
+static int globerr(const char *, int);
+
+static char *warnings[] = {
+ "-Wall",
+ "-Wextra",
+ "-Wpedantic",
+ "-Wno-parentheses",
+};
+
+static char *cflags_all[] = {
+ "-std=c11",
+#if __GLIBC__
+ "-D_GNU_SOURCE",
+#endif
+};
+
+static char *cflags_dbg[] = {
+ "-g3",
+ "-ggdb3",
+ "-O0",
+};
+
+static char *cflags_rls[] = {
+ "-DNDEBUG=1",
+ "-flto",
+ "-fomit-frame-pointer",
+ "-march=native",
+ "-mtune=native",
+ "-O3",
+};
+
+static const char *argv0;
+static bool fflag, Sflag, rflag;
+static char *oflag = "totp", *pflag = "generic";
+
+static void
+usage(void)
+{
+ fprintf(stderr,
+ "Usage: %s [-p generic|x64] [-fSr]\n"
+ " %s clean\n",
+ argv0, argv0);
+ exit(EXIT_FAILURE);
+}
+
+int
+main(int argc, char **argv)
+{
+ cbsinit(argc, argv);
+ rebuild();
+
+ argv0 = argv[0];
+
+ int opt;
+ static const struct option longopts[] = {
+ {"force", no_argument, 0, 'f'},
+ {"no-sanitizer", no_argument, 0, 'S'},
+ {"output", required_argument, 0, 'o'},
+ {"profile", required_argument, 0, 'p'},
+ {"release", no_argument, 0, 'r'},
+ {0},
+ };
+
+ while ((opt = getopt_long(argc, argv, "fSo:p:r", longopts, NULL)) != -1) {
+ switch (opt) {
+ case 'f':
+ fflag = true;
+ break;
+ case 'r':
+ rflag = true;
+ /* fallthrough */
+ case 'S':
+ Sflag = true;
+ break;
+ case 'o':
+ oflag = strdup(optarg);
+ assert(oflag != NULL);
+ break;
+ case 'p':
+ if (strcmp(optarg, "generic") == 0 || strcmp(optarg, "x64") == 0) {
+ pflag = strdup(optarg);
+ assert(pflag != NULL);
+ } else {
+ fprintf(stderr, "%s: invalid profile -- '%s'\n", argv0, optarg);
+ usage();
+ }
+ break;
+ default:
+ usage();
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (argc > 1)
+ usage();
+ if (argc == 1) {
+ if (strcmp(argv[0], "clean") != 0) {
+ fprintf(stderr, "%s: invalid subcommand -- '%s'\n", argv0, *argv);
+ usage();
+ }
+ struct strs cmd = {0};
+ strspushl(&cmd, "find", ".",
+ "(",
+ "-name", "totp",
+ "-or", "-name", "totp-*",
+ "-or", "-name", "*.o",
+ ")", "-delete"
+ );
+ cmdput(cmd);
+ return cmdexec(cmd);
+ }
+
+ glob_t g;
+ assert(glob("src/*.c", 0, globerr, &g) == 0);
+
+ char *ext = malloc(strlen(pflag) + sizeof("-.c"));
+ assert(ext != NULL);
+ sprintf(ext, "-%s.c", pflag);
+
+ for (size_t i = 0; i < g.gl_pathc; i++) {
+ if (strchr(g.gl_pathv[i], '-') != NULL
+ && strstr(g.gl_pathv[i], ext) == NULL)
+ {
+ continue;
+ }
+ cc(g.gl_pathv[i]);
+ }
+
+ globfree(&g);
+
+ ld();
+}
+
+void
+cc(void *arg)
+{
+ struct strs cmd = {0};
+ char *dst = swpext(arg, "o"), *src = arg;
+
+ if (!fflag && fmdnewer(dst, src))
+ goto out;
+
+ strspushenvl(&cmd, "CC", "cc");
+ strspush(&cmd, cflags_all, lengthof(cflags_all));
+ if (rflag)
+ strspushenv(&cmd, "CFLAGS", cflags_rls, lengthof(cflags_rls));
+ else {
+ strspushenv(&cmd, "CFLAGS", cflags_dbg, lengthof(cflags_dbg));
+ if (strstr(arg, "-x64.c") != NULL)
+ strspushl(&cmd, "-msha", "-mssse3");
+ }
+ if (!Sflag)
+ strspushl(&cmd, "-fsanitize=address,undefined");
+ strspushl(&cmd, "-o", dst, "-c", src);
+
+ cmdput(cmd);
+ cmdexec(cmd);
+ strsfree(&cmd);
+out:
+ free(dst);
+}
+
+void
+ld(void)
+{
+ glob_t g;
+ bool dobuild = fflag;
+ struct strs cmd = {0};
+
+ strspushenvl(&cmd, "CC", "cc");
+ strspush(&cmd, cflags_all, lengthof(cflags_all));
+ if (rflag)
+ strspushenv(&cmd, "CFLAGS", cflags_rls, lengthof(cflags_rls));
+ else
+ strspushenv(&cmd, "CFLAGS", cflags_dbg, lengthof(cflags_dbg));
+ if (!Sflag)
+ strspushl(&cmd, "-fsanitize=address,undefined");
+ strspushl(&cmd, "-o", oflag);
+
+ assert(glob("src/*.o", 0, globerr, &g) == 0);
+
+ char *ext = malloc(strlen(pflag) + sizeof("-.o"));
+ assert(ext != NULL);
+ sprintf(ext, "-%s.o", pflag);
+
+ for (size_t i = 0; i < g.gl_pathc; i++) {
+ if (strchr(g.gl_pathv[i], '-') != NULL
+ && strstr(g.gl_pathv[i], ext) == NULL)
+ {
+ continue;
+ }
+ if (fmdolder("totp", g.gl_pathv[i]))
+ dobuild = true;
+ strspushl(&cmd, g.gl_pathv[i]);
+ }
+
+ if (dobuild) {
+ cmdput(cmd);
+ cmdexec(cmd);
+ }
+
+ globfree(&g);
+ strsfree(&cmd);
+}
+
+int
+globerr(const char *s, int e)
+{
+ fprintf(stderr, "glob: %s: %s\n", s, strerror(e));
+ exit(EXIT_FAILURE);
+}
diff --git a/src/base32.c b/src/base32.c
new file mode 100644
index 0000000..82c5b48
--- /dev/null
+++ b/src/base32.c
@@ -0,0 +1,93 @@
+#include <assert.h>
+
+#include "base32.h"
+#include "common.h"
+
+static inline bool b32blktoa(uint8_t *restrict, const uint8_t *restrict)
+ __attribute__((always_inline));
+
+static const uint8_t lookup[] = {
+ /* [00…07] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [08…0F] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [10…17] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [18…1F] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [20…27] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [28…2F] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [30…37] = */ 0xFF, 0xFF, 26, 27, 28, 29, 30, 31,
+ /* [38…3F] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0xFF, 0xFF,
+ /* [40…47] = */ 0xFF, 0, 1, 2, 3, 4, 5, 6,
+ /* [48…4F] = */ 7, 8, 9, 10, 11, 12, 13, 14,
+ /* [50…57] = */ 15, 16, 17, 18, 19, 20, 21, 22,
+ /* [58…5F] = */ 23, 24, 25, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [60…67] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [68…6F] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [70…77] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [78…7F] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [80…87] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [88…8F] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [90…97] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [98…9F] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [A0…A7] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [A8…AF] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [B0…B7] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [B8…BF] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [C0…C7] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [C8…CF] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [D0…D7] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [D8…DF] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [E0…E7] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [E8…EF] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [F0…F7] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ /* [F8…FF] = */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+};
+
+bool
+b32toa(uint8_t *restrict dst, const char *restrict src, size_t len)
+{
+ assert(len != 0);
+
+ size_t i, j;
+ for (i = j = 0; len - i >= 8; i += 8, j += 5) {
+ uint8_t bits[] = {
+ lookup[(uint8_t)src[i + 0]],
+ lookup[(uint8_t)src[i + 1]],
+ lookup[(uint8_t)src[i + 2]],
+ lookup[(uint8_t)src[i + 3]],
+ lookup[(uint8_t)src[i + 4]],
+ lookup[(uint8_t)src[i + 5]],
+ lookup[(uint8_t)src[i + 6]],
+ lookup[(uint8_t)src[i + 7]],
+ };
+ if (!b32blktoa(dst + j, bits))
+ return false;
+ }
+
+ uint8_t bits[8] = {0};
+ switch (len - i) {
+ case 7: bits[6] = lookup[(uint8_t)src[i + 6]]; /* fallthrough */
+ case 6: bits[5] = lookup[(uint8_t)src[i + 5]]; /* fallthrough */
+ case 5: bits[4] = lookup[(uint8_t)src[i + 4]]; /* fallthrough */
+ case 4: bits[3] = lookup[(uint8_t)src[i + 3]]; /* fallthrough */
+ case 3: bits[2] = lookup[(uint8_t)src[i + 2]]; /* fallthrough */
+ case 2: bits[1] = lookup[(uint8_t)src[i + 1]]; /* fallthrough */
+ case 1: bits[0] = lookup[(uint8_t)src[i + 0]];
+ return b32blktoa(dst + j, bits);
+ }
+ return true;
+}
+
+bool
+b32blktoa(uint8_t *restrict dst, const uint8_t *restrict src)
+{
+ uint8_t or = src[0] | src[1] | src[2] | src[3]
+ | src[4] | src[5] | src[6] | src[7];
+ if (or == 0xFF)
+ return false;
+
+ dst[0] = src[0]<<3 | src[1]>>2;
+ dst[1] = src[1]<<6 | src[2]<<1 | src[3]>>4;
+ dst[2] = src[3]<<4 | src[4]>>1;
+ dst[3] = src[4]<<7 | src[5]<<2 | src[6]>>3;
+ dst[4] = src[6]<<5 | src[7]>>0;
+ return true;
+}
diff --git a/src/base32.h b/src/base32.h
new file mode 100644
index 0000000..2581878
--- /dev/null
+++ b/src/base32.h
@@ -0,0 +1,10 @@
+#ifndef TOTP_BASE32_H
+#define TOTP_BASE32_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+bool b32toa(uint8_t *restrict, const char *restrict, size_t);
+
+#endif /* !TOTP_BASE32_H */
diff --git a/src/common.h b/src/common.h
new file mode 100644
index 0000000..c1d21c8
--- /dev/null
+++ b/src/common.h
@@ -0,0 +1,27 @@
+#ifndef TOTP_COMMON_H
+#define TOTP_COMMON_H
+
+#if !__GNUC__
+# define __attribute__(x)
+#endif
+
+/* TODO: Is this endian stuff potentially useful? */
+
+/* If C23 or newer include this to get byte-order macros */
+#if __STDC_VERSION__ >= 202311L
+# include <stdbit.h>
+#endif
+
+#if (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) \
+ || (defined(__STDC_ENDIAN_NATIVE__) \
+ && __STDC_ENDIAN_NATIVE__ == __STDC_ENDIAN_BIG__)
+# define ENDIAN_BIG 1
+#elif (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) \
+ || (defined(__STDC_ENDIAN_NATIVE__) \
+ && __STDC_ENDIAN_NATIVE__ == __STDC_ENDIAN_LITTLE__)
+# define ENDIAN_LITTLE 1
+#else
+# define ENDIAN_UNKNOWN 1
+#endif
+
+#endif /* !TOTP_COMMON_H */
diff --git a/src/hmac.c b/src/hmac.c
new file mode 100644
index 0000000..5175ee2
--- /dev/null
+++ b/src/hmac.c
@@ -0,0 +1,41 @@
+#include <string.h>
+
+#include "sha1.h"
+
+#define IPAD (0x36)
+#define OPAD (0x5C)
+
+void
+hmac_sha1(uint8_t *restrict out,
+ const uint8_t *restrict key, size_t keysz,
+ const uint8_t *restrict msg, size_t msgsz)
+{
+ uint8_t keyext[SHA1BLKSZ] = {0},
+ keyipad[SHA1BLKSZ],
+ keyopad[SHA1BLKSZ];
+
+ if (keysz > SHA1BLKSZ) {
+ sha1_t sha;
+ sha1init(&sha);
+ sha1hash(&sha, key, keysz);
+ sha1end(&sha, keyext);
+ } else
+ memcpy(keyext, key, keysz);
+
+ for (size_t i = 0; i < sizeof(keyext); i++) {
+ keyipad[i] = keyext[i] ^ IPAD;
+ keyopad[i] = keyext[i] ^ OPAD;
+ }
+
+ sha1_t sha;
+ uint8_t dgst[SHA1DGSTSZ];
+ sha1init(&sha);
+ sha1hash(&sha, keyipad, sizeof(keyipad));
+ sha1hash(&sha, msg, msgsz);
+ sha1end(&sha, dgst);
+
+ sha1init(&sha);
+ sha1hash(&sha, keyopad, sizeof(keyopad));
+ sha1hash(&sha, dgst, sizeof(dgst));
+ sha1end(&sha, out);
+}
diff --git a/src/hmac.h b/src/hmac.h
new file mode 100644
index 0000000..3c3e8e7
--- /dev/null
+++ b/src/hmac.h
@@ -0,0 +1,11 @@
+#ifndef TOTP_HMAC_H
+#define TOTP_HMAC_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+void hmac_sha1(uint8_t *restrict,
+ const uint8_t *restrict, size_t,
+ const uint8_t *restrict, size_t);
+
+#endif /* !TOTP_HMAC_H */
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..40318cf
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,191 @@
+#include <err.h>
+#include <errno.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdnoreturn.h>
+#include <string.h>
+#include <time.h>
+
+#include "base32.h"
+#include "common.h"
+#include "hmac.h"
+#include "sha1.h"
+#include "xendian.h"
+
+static void process(const char *, size_t);
+static void process_stdin(void);
+static inline uint32_t pow32(uint32_t, uint32_t)
+ __attribute__((always_inline, const));
+static inline bool xisdigit(char)
+ __attribute__((always_inline, const));
+static inline bool bigendian(void)
+ __attribute__((always_inline, const));
+
+static int digits = 6, period = 30;
+
+static noreturn void
+usage(const char *argv0)
+{
+ fprintf(stderr,
+ "Usage: %s [-d digits] [-p period] [secret]\n"
+ " %s -h\n",
+ argv0, argv0);
+ exit(EXIT_FAILURE);
+}
+
+int
+main(int argc, char **argv)
+{
+ int opt;
+ static const struct option longopts[] = {
+ {"digits", required_argument, 0, 'd'},
+ {"period", required_argument, 0, 'p'},
+ {0},
+ };
+
+ argv[0] = basename(argv[0]);
+ while ((opt = getopt_long(argc, argv, "d:p:", longopts, NULL)) != -1) {
+ switch (opt) {
+ case 'd':
+ case 'p': {
+ /* strtol() allows for numbers with leading spaces and a
+ ‘+’/‘-’. We don’t want that, so assert that the input
+ begins with a number. */
+ if (!xisdigit(optarg[0]))
+ errx(1, "%s: Invalid integer", optarg);
+
+ errno = 0;
+ char *endptr;
+ long n = strtol(optarg, &endptr, 10);
+
+ /* There are trailing invalid digits */
+ if (*endptr != 0)
+ errx(1, "%s: Invalid integer", optarg);
+
+ /* The number was too large. We asserted that the input
+ didn’t start with ‘-’ so we can ignore checking for
+ LONG_MIN. */
+ if (n > INT_MAX)
+ errno = ERANGE;
+ if (errno == ERANGE)
+ err(1, "%s", optarg);
+
+ if (n == 0)
+ errx(1, "%s: Integer must be non-zero", optarg);
+ if (opt == 'd')
+ digits = (int)n;
+ else
+ period = (int)n;
+ break;
+ }
+ default:
+ usage(argv[0]);
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ switch (argc) {
+ case 0:
+ process_stdin();
+ break;
+ case 1:
+ process(argv[0], strlen(argv[0]));
+ break;
+ default:
+ usage(argv[-optind]);
+ }
+
+ return EXIT_SUCCESS;
+}
+
+void
+process_stdin(void)
+{
+ ssize_t nr;
+ size_t len;
+ char *line = NULL;
+ while ((nr = getline(&line, &len, stdin)) != -1) {
+ if (line[nr - 1] == '\n')
+ line[--nr] = 0;
+ process(line, nr);
+ }
+ if (errno != 0)
+ err(1, "getline");
+}
+
+void
+process(const char *s, size_t n)
+{
+ /* Remove padding bytes */
+ while (n > 0 && s[n - 1] == '=')
+ n--;
+ if (n == 0)
+ errx(1, "Empty Base32 input");
+
+ static uint8_t _key[256];
+ uint8_t *key = _key;
+
+ size_t keysz = n * 5 / 8;
+ if (keysz > sizeof(_key)) {
+ if ((key = malloc(keysz)) == NULL)
+ err(1, "malloc");
+ }
+
+ if (!b32toa(key, s, n))
+ errx(1, "%s: Invalid Base32 input", s);
+
+ /* time(2) claims that this call will never fail if passed a NULL
+ argument. We cast the time_t to uint64_t which will always be
+ safe to do. */
+ uint64_t epoch = htobe64((uint64_t)time(NULL) / (uint64_t)period);
+ uint8_t dgst[SHA1DGSTSZ];
+ hmac_sha1(dgst, key, keysz, (uint8_t *)&epoch, sizeof(epoch));
+
+ int off = dgst[19] & 0x0F;
+ uint32_t binc = (dgst[off + 0] & 0x7F) << 24
+ | (dgst[off + 1] & 0xFF) << 16
+ | (dgst[off + 2] & 0xFF) << 8
+ | (dgst[off + 3] & 0xFF) << 0;
+ printf("%0*" PRId32 "\n", digits, binc % pow32(10, digits));
+
+ if (key != _key)
+ free(key);
+}
+
+/* TODO: Check for overflow? */
+uint32_t
+pow32(uint32_t x, uint32_t y)
+{
+ uint32_t n = x;
+ if (y == 0)
+ return 1;
+ while (--y != 0)
+ x *= n;
+ return x;
+}
+
+bool
+xisdigit(char ch)
+{
+ return ch >= '0' && ch <= '9';
+}
+
+bool
+bigendian(void)
+{
+ union {
+ uint16_t u16;
+ uint8_t u8[2];
+ } u = {
+ .u16 = 0x0102,
+ };
+ u.u16 = 0x0102U;
+ return u.u8[0] == 1;
+}
diff --git a/src/sha1-generic.c b/src/sha1-generic.c
new file mode 100644
index 0000000..d897a8f
--- /dev/null
+++ b/src/sha1-generic.c
@@ -0,0 +1,78 @@
+#include "common.h"
+#include "sha1.h"
+#include "xendian.h"
+
+static inline uint32_t rotl32(uint32_t x, uint8_t bits)
+ __attribute__((always_inline, const));
+
+static const uint32_t K[] = {
+ 0x5A827999,
+ 0x6ED9EBA1,
+ 0x8F1BBCDC,
+ 0xCA62C1D6,
+};
+
+void
+sha1hashblk(sha1_t *s, const uint8_t *blk)
+{
+ uint32_t w[80];
+ uint32_t a, b, c, d, e, tmp;
+
+ for (int i = 0; i < 16; i++)
+ w[i] = htobe32(((uint32_t *)blk)[i]);
+ for (int i = 16; i < 32; i++)
+ w[i] = rotl32(w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16], 1);
+ for (int i = 32; i < 80; i++)
+ w[i] = rotl32(w[i-6] ^ w[i-16] ^ w[i-28] ^ w[i-32], 2);
+
+ a = s->dgst[0];
+ b = s->dgst[1];
+ c = s->dgst[2];
+ d = s->dgst[3];
+ e = s->dgst[4];
+
+ for (int i = 0; i < 80; i++) {
+ uint32_t f, k;
+
+ if (i < 20) {
+ f = b&c | ~b&d;
+ k = K[0];
+ } else if (i < 40) {
+ f = b ^ c ^ d;
+ k = K[1];
+ } else if (i < 60) {
+ f = b&c | b&d | c&d;
+ k = K[2];
+ } else {
+ f = b ^ c ^ d;
+ k = K[3];
+ }
+
+ tmp = rotl32(a, 5) + f + e + w[i] + k;
+ e = d;
+ d = c;
+ c = rotl32(b, 30);
+ b = a;
+ a = tmp;
+ }
+
+ s->dgst[0] += a;
+ s->dgst[1] += b;
+ s->dgst[2] += c;
+ s->dgst[3] += d;
+ s->dgst[4] += e;
+}
+
+uint32_t
+rotl32(uint32_t x, uint8_t bits)
+{
+#if (__GNUC__ || __TINYC__) && __x86_64__
+ __asm__ ("roll %1, %0" : "+r" (x) : "c" (bits) : "cc");
+ return x;
+#elif __GNUC__ && __aarch64__ /* TODO: Test this! */
+ __asm__ ("ror %0, %0, %1" : "+r" (x) : "c" (-bits));
+ return x;
+#else
+ return (x << bits) | (x >> (32 - bits));
+#endif
+}
diff --git a/src/sha1-x64.c b/src/sha1-x64.c
new file mode 100644
index 0000000..be19ab7
--- /dev/null
+++ b/src/sha1-x64.c
@@ -0,0 +1,98 @@
+#include <immintrin.h>
+
+#include "sha1.h"
+
+#define R(mi, mj, mk, ml, ei, ej, f) \
+ do { \
+ ei = _mm_sha1nexte_epu32(ei, mi); \
+ ej = abcd; \
+ mj = _mm_sha1msg2_epu32(mj, mi); \
+ abcd = _mm_sha1rnds4_epu32(abcd, ei, f); \
+ ml = _mm_sha1msg1_epu32(ml, mi); \
+ mk = _mm_xor_si128(mk, mi); \
+ } while (0)
+
+void
+sha1hashblk(sha1_t *s, const uint8_t *blk)
+{
+ __m128i abcd, e0, e1;
+ __m128i abcd_save, e_save;
+ __m128i msg0, msg1, msg2, msg3;
+
+ /* Masks for swapping endianness. We make BSWAPDMSK a macro to
+ please the compiler (it wants immediate values). */
+#define bswapdmsk 0x1B /* 0b00'01'10'11 */
+ const __m128i bswapbmsk = _mm_set_epi64x(
+ 0x0001020304050607ULL,
+ 0x08090a0b0c0d0e0fULL
+ );
+
+ const __m128i *blkx = (const __m128i *)blk;
+
+ abcd = _mm_shuffle_epi32(_mm_loadu_si128((__m128i *)s->dgst), bswapdmsk);
+ e0 = _mm_set_epi32(s->dgst[4], 0, 0, 0);
+
+ abcd_save = abcd;
+ e_save = e0;
+
+ /* Rounds 0–3 */
+ msg0 = _mm_shuffle_epi8(_mm_loadu_si128(blkx + 0), bswapbmsk);
+ e0 = _mm_add_epi32(e0, msg0);
+ e1 = abcd;
+ abcd = _mm_sha1rnds4_epu32(abcd, e0, 0);
+
+ /* Rounds 4–7 */
+ msg1 = _mm_shuffle_epi8(_mm_loadu_si128(blkx + 1), bswapbmsk);
+ e1 = _mm_sha1nexte_epu32(e1, msg1);
+ e0 = abcd;
+ abcd = _mm_sha1rnds4_epu32(abcd, e1, 0);
+ msg0 = _mm_sha1msg1_epu32(msg0, msg1);
+
+ /* Rounds 8–11 */
+ msg2 = _mm_shuffle_epi8(_mm_loadu_si128(blkx + 2), bswapbmsk);
+ e0 = _mm_sha1nexte_epu32(e0, msg2);
+ e1 = abcd;
+ abcd = _mm_sha1rnds4_epu32(abcd, e0, 0);
+ msg1 = _mm_sha1msg1_epu32(msg1, msg2);
+ msg0 = _mm_xor_si128(msg0, msg2);
+
+ msg3 = _mm_shuffle_epi8(_mm_loadu_si128(blkx + 3), bswapbmsk);
+ R(msg3, msg0, msg1, msg2, e1, e0, 0); /* Rounds 12–15 */
+ R(msg0, msg1, msg2, msg3, e0, e1, 0); /* Rounds 16–19 */
+ R(msg1, msg2, msg3, msg0, e1, e0, 1); /* Rounds 20–23 */
+ R(msg2, msg3, msg0, msg1, e0, e1, 1); /* Rounds 24–27 */
+ R(msg3, msg0, msg1, msg2, e1, e0, 1); /* Rounds 28–31 */
+ R(msg0, msg1, msg2, msg3, e0, e1, 1); /* Rounds 32–35 */
+ R(msg1, msg2, msg3, msg0, e1, e0, 1); /* Rounds 36–39 */
+ R(msg2, msg3, msg0, msg1, e0, e1, 2); /* Rounds 40–43 */
+ R(msg3, msg0, msg1, msg2, e1, e0, 2); /* Rounds 44–47 */
+ R(msg0, msg1, msg2, msg3, e0, e1, 2); /* Rounds 48–51 */
+ R(msg1, msg2, msg3, msg0, e1, e0, 2); /* Rounds 52–55 */
+ R(msg2, msg3, msg0, msg1, e0, e1, 2); /* Rounds 56–59 */
+ R(msg3, msg0, msg1, msg2, e1, e0, 3); /* Rounds 60–63 */
+ R(msg0, msg1, msg2, msg3, e0, e1, 3); /* Rounds 64–67 */
+
+ /* Rounds 68–71 */
+ e1 = _mm_sha1nexte_epu32(e1, msg1);
+ e0 = abcd;
+ msg2 = _mm_sha1msg2_epu32(msg2, msg1);
+ abcd = _mm_sha1rnds4_epu32(abcd, e1, 3);
+ msg3 = _mm_xor_si128(msg3, msg1);
+
+ /* Rounds 72–75 */
+ e0 = _mm_sha1nexte_epu32(e0, msg2);
+ e1 = abcd;
+ msg3 = _mm_sha1msg2_epu32(msg3, msg2);
+ abcd = _mm_sha1rnds4_epu32(abcd, e0, 3);
+
+ /* Rounds 76–79 */
+ e1 = _mm_sha1nexte_epu32(e1, msg3);
+ e0 = abcd;
+ abcd = _mm_sha1rnds4_epu32(abcd, e1, 3);
+
+ e0 = _mm_sha1nexte_epu32(e0, e_save);
+ abcd = _mm_add_epi32(abcd, abcd_save);
+
+ _mm_storeu_si128((__m128i *)s->dgst, _mm_shuffle_epi32(abcd, bswapdmsk));
+ s->dgst[4] = _mm_extract_epi32(e0, 3);
+}
diff --git a/src/sha1.c b/src/sha1.c
new file mode 100644
index 0000000..5759991
--- /dev/null
+++ b/src/sha1.c
@@ -0,0 +1,76 @@
+#include <err.h>
+#include <errno.h>
+#include <string.h>
+
+#include "sha1.h"
+#include "xendian.h"
+
+#define lengthof(x) (sizeof(x) / sizeof(*(x)))
+#define MIN(x, y) ((x) < (y) ? (x) : (y))
+
+void sha1hashblk(sha1_t *, const uint8_t *);
+
+void
+sha1init(sha1_t *s)
+{
+ static const uint32_t H[] = {
+ 0x67452301,
+ 0xEFCDAB89,
+ 0x98BADCFE,
+ 0x10325476,
+ 0xC3D2E1F0,
+ };
+ memcpy(s->dgst, H, sizeof(H));
+ s->msgsz = s->bufsz = 0;
+}
+
+void
+sha1hash(sha1_t *s, const uint8_t *msg, size_t msgsz)
+{
+ if (s->msgsz + (msgsz * 8) < s->msgsz) {
+ errno = EOVERFLOW;
+ err(1, "sha1");
+ }
+
+ s->msgsz += msgsz * 8;
+
+ while (msgsz != 0) {
+ size_t free_space = SHA1BLKSZ - s->bufsz;
+ size_t ncpy = MIN(msgsz, free_space);
+ memcpy(s->buf + s->bufsz, msg, ncpy);
+ s->bufsz += ncpy;
+ msg += ncpy;
+ msgsz -= ncpy;
+
+ if (s->bufsz == SHA1BLKSZ) {
+ sha1hashblk(s, s->buf);
+ s->bufsz = 0;
+ }
+ }
+}
+
+void
+sha1end(sha1_t *s, uint8_t *dgst)
+{
+ s->buf[s->bufsz++] = 0x80;
+
+ if (s->bufsz > SHA1BLKSZ - sizeof(uint64_t)) {
+ while (s->bufsz < SHA1BLKSZ)
+ s->buf[s->bufsz++] = 0;
+ sha1hashblk(s, s->buf);
+ s->bufsz = 0;
+ }
+
+ while (s->bufsz < 56)
+ s->buf[s->bufsz++] = 0;
+ uint64_t n = htobe64(s->msgsz);
+ memcpy(s->buf + (SHA1BLKSZ/8 - 1)*sizeof(uint64_t), &n, sizeof(n));
+
+ sha1hashblk(s, s->buf);
+
+ for (size_t i = 0; i < lengthof(s->dgst); i++) {
+ /* Pretty please compiler optimize this */
+ uint32_t n = htobe32(s->dgst[i]);
+ memcpy(dgst + i*sizeof(uint32_t), &n, sizeof(n));
+ }
+}
diff --git a/src/sha1.h b/src/sha1.h
new file mode 100644
index 0000000..ea08d37
--- /dev/null
+++ b/src/sha1.h
@@ -0,0 +1,21 @@
+#ifndef TOTP_SHA1_H
+#define TOTP_SHA1_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#define SHA1DGSTSZ (20)
+#define SHA1BLKSZ (64)
+
+typedef struct {
+ uint32_t dgst[SHA1DGSTSZ / sizeof(uint32_t)];
+ uint64_t msgsz;
+ uint8_t buf[SHA1BLKSZ];
+ size_t bufsz;
+} sha1_t;
+
+void sha1init(sha1_t *);
+void sha1hash(sha1_t *, const uint8_t *, size_t);
+void sha1end(sha1_t *, uint8_t *);
+
+#endif /* !TOTP_SHA1_H */
diff --git a/src/xendian.h b/src/xendian.h
new file mode 100644
index 0000000..b43661f
--- /dev/null
+++ b/src/xendian.h
@@ -0,0 +1,21 @@
+#ifndef TOTP_XENDIAN_H
+#define TOTP_XENDIAN_H
+
+/* This header grabs the htobe64() and co. functions in a more
+ cross-platform manner. In general you will find these functions in
+ <sys/endian.h>, however Linux and OpenBSD include them in <endian.h>.
+ To make things even better this header doesn’t exist on MacOS so we
+ need to define wrapper macros for the htonXX() functions from
+ <arpa/inet.h>. */
+
+#if defined(__OpenBSD__) || defined(__linux__)
+# include <endian.h>
+#elif defined(__APPLE__)
+# include <arpa/inet.h>
+# define htobe32(x) htonl(x)
+# define htobe64(x) htonll(x)
+#else
+# include <sys/endian.h>
+#endif
+
+#endif /* !TOTP_XENDIAN_H */