totp

Simple cli tool for storing TOTP secrets and generating tokens
git clone https://git.inz.fi/totp/
Log | Files | Refs | Submodules

commit 4038e852c1e13726ddb3e3e7394e237188b0bfe6
parent 9ff6f1b75fdd5361a97a48be1a31f701abb99e14
Author: Santtu Lakkala <santtu.lakkala@digital14.com>
Date:   Tue, 12 Sep 2023 19:01:16 +0300

Semi-major rewrite

Split token handling and filew operations to separate compilation units.
Drop usage of byte order conversions, and possibly accessing invalid
alignment variables.

Diffstat:
MMakefile | 42++++++++++++++++++++++++++++++++++++------
Adb.c | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adb.h | 31+++++++++++++++++++++++++++++++
Mmain.c | 499++++++++++++++++---------------------------------------------------------------
Msha512.c | 12+++++++-----
Atoken.c | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atoken.h | 33+++++++++++++++++++++++++++++++++
Mutil.c | 47+++++++++++++++++++++++++++++++++++++++--------
Mutil.h | 69+++++++++++++++++++++++++++++++++++++++++++--------------------------
9 files changed, 622 insertions(+), 448 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,6 +1,6 @@ -CFLAGS = -g -W -Wall -std=c99 +CFLAGS = -W -Wall -std=c99 -g -Os AES_CFLAGS += -DECB=0 -DCBC=1 -DCTR=0 -DAES256=1 -SOURCES = sha1.c sha256.c sha512.c tiny-AES-c/aes.c main.c util.c +SOURCES = sha1.c sha256.c sha512.c tiny-AES-c/aes.c main.c util.c db.c token.c OBJS = ${SOURCES:.c=.o} TEST_SOURCES = sha1.c sha256.c sha512.c util.c test.c TEST_OBJS = ${TEST_SOURCES:.c=.o} @@ -14,17 +14,17 @@ NAME=totp all: ${NAME} -totp: ${OBJS} - ${CC} -o $@ ${OBJS} ${LDFLAGS} +${NAME}: ${OBJS} + ${CC} ${CFLAGS} -o $@ ${OBJS} ${LDFLAGS} test: ${TEST_OBJS}; - ${CC} -o $@ ${TEST_OBJS} ${LDFLAGS} + ${CC} ${CFLAGS} -o $@ ${TEST_OBJS} ${LDFLAGS} .c.o: ${CC} -c $< -o $@ ${CFLAGS} ${AES_CFLAGS} clean: - rm ${OBJS} + rm -f ${OBJS} install: all mkdir -p "${DESTDIR}${BINDIR}" @@ -33,3 +33,33 @@ install: all mkdir -p "${DESTDIR}${MANDIR}" cp -f "${NAME}.1" "${DESTDIR}${MANDIR}" chmod 644 "${DESTDIR}${MANDIR}/${NAME}.1" + +db.o: db.h +db.o: tiny-AES-c/aes.h +db.o: token.h +db.o: util.h +main.o: arg.h +main.o: db.h +main.o: sha1.h +main.o: sha256.h +main.o: sha512.h +main.o: tiny-AES-c/aes.h +main.o: token.h +main.o: util.h +sha1.o: sha1.h +sha1.o: util.h +sha256.o: sha256.h +sha256.o: util.h +sha512.o: sha512.h +sha512.o: util.h +test.o: sha1.h +test.o: sha256.h +test.o: sha512.h +test.o: util.h +token.o: token.h +token.o: util.h +util.o: util.h +db.h: token.h +token.h: util.h + +${OBJS}: Makefile diff --git a/db.c b/db.c @@ -0,0 +1,195 @@ +#include <stdbool.h> +#include <stdint.h> + +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> + +#include "db.h" +#include "util.h" +#include "token.h" +#include "tiny-AES-c/aes.h" + +struct header { + uint8_t magic[4]; + uint8_t version; +}; + +static bool verify_db(int fd, struct AES_ctx *c) +{ + uint8_t rbuf[AES_BLOCKLEN]; + int r; + size_t rused = 0; + struct header *h; + + while ((r = read(fd, rbuf + rused, sizeof(rbuf) - rused)) > 0) + rused += r; + + if (rused < sizeof(rbuf)) { + errno = ENODATA; + return false; + } + + AES_CBC_decrypt_buffer(c, rbuf, sizeof(rbuf)); + h = (struct header *)(rbuf + rbuf[0] % (sizeof(rbuf) - sizeof(*h) - 1) + 1); + + if (h->magic[0] == 'T' && + h->magic[1] == 'O' && + h->magic[2] == 'T' && + h->magic[3] == 'P' && + h->version == 1) + return true; + + errno = EPERM; + return false; +} + +int db_open_read(const char *filename, struct AES_ctx *c) { + int fd = open(filename, O_RDONLY); + + if (fd < 0) + return fd; + + if (verify_db(fd, c)) + return fd; + + close(fd); + return -1; +} + +static int write_header(int fd, struct AES_ctx *c) +{ + uint8_t wbuf[AES_BLOCKLEN]; + int w; + uint8_t *wp = wbuf; + uint8_t * const wend = 1[&wbuf]; + struct header *h; + + randmem(wbuf, sizeof(wbuf)); + + h = (struct header *)(wbuf + wbuf[0] % (sizeof(wbuf) - sizeof(*h) - 1) + 1); + + h->magic[0] = 'T'; + h->magic[1] = 'O'; + h->magic[2] = 'T'; + h->magic[3] = 'P'; + h->version = 1; + + AES_CBC_encrypt_buffer(c, wbuf, sizeof(wbuf)); + + while (wp < wend && (w = write(fd, wp, wend - wp)) > 0) + wp += w; + + if (w < 0) + return -1; + + if (wp < wend) { + errno = EIO; + return -1; + } + + return 0; + +} + +int db_open_write(const char *filename, struct AES_ctx *c) +{ + int fd = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0600); + + if (fd < 0) + return fd; + + if (write_header(fd, c)) { + int tmp_errno = errno; + (void)unlink(filename); + (void)close(fd); + errno = tmp_errno; + + return -1; + } + + return fd; +} + +void db_foreach(int fd, struct AES_ctx *c, + void (*key_cb)(struct token *token, + void *data), + void *cb_data) +{ + uint8_t decbuf[512]; + uint8_t rbuf[AES_BLOCKLEN]; + uint8_t *dp = decbuf; + uint8_t * const dend = 1[&decbuf]; + uint8_t *rp = rbuf; + uint8_t * const rend = 1[&rbuf]; + int r; + + while ((r = read(fd, rp, rend - rp)) > 0) { + struct totpkey *kh = (struct totpkey *)decbuf; + if ((rp += r) < rend) + continue; + AES_CBC_decrypt_buffer(c, rbuf, sizeof(rbuf)); + if (dp + sizeof(rbuf) >= dend) + break; + dp = mempush(dp, rp = rbuf, sizeof(rbuf)); + + if (decbuf + sizeof(*kh) > dp || + decbuf + sizeof(*kh) + kh->keylen + kh->desclen + kh->issuerlen > dp) + continue; + + struct bytes key = { decbuf + sizeof(*kh), kh->keylen }; + struct bytes desc = { key.data + key.len, kh->desclen }; + struct bytes issuer = { desc.data + desc.len, kh->issuerlen }; + key_cb(&(struct token){ + key, desc, issuer, + kh->digest, readbeu64(kh->t0), + kh->digits, kh->period, + true + }, cb_data); + + dp = decbuf; + } +} + +int db_add_key(int fd, struct AES_ctx *c, + struct token *token) +{ + size_t ksz = sizeof(struct totpkey) + token->key.len + token->desc.len + token->issuer.len; + size_t i; + int w; + + ksz = (ksz + AES_BLOCKLEN - 1) / AES_BLOCKLEN * AES_BLOCKLEN; + + if (token->key.len > UINT8_MAX || token->desc.len > UINT8_MAX || token->issuer.len > UINT8_MAX) { + errno = EMSGSIZE; + return -1; + } + + uint8_t buffer[1024]; + uint8_t *wp = buffer; + uint64_t t0 = token->t0; + wp = mempush(wp, &(struct totpkey){ + .t0 = { t0 >> 56, t0 >> 48, t0 >> 40, t0 >> 32, t0 >> 24, t0 >> 16, t0 >> 8, t0 }, + .digest = token->digest, + .digits = token->digits, + .period = token->period, + .keylen = token->key.len, + .desclen = token->desc.len, + .issuerlen = token->issuer.len }, sizeof(struct totpkey)); + wp = mempushb(wp, token->key); + wp = mempushb(wp, token->desc); + wp = mempushb(wp, token->issuer); + randmem(wp, buffer + ksz - wp); + + for (i = 0; i < ksz; i += AES_BLOCKLEN) + AES_CBC_encrypt_buffer(c, buffer + i, AES_BLOCKLEN); + i = 0; + + while ((w = write(fd, buffer + i, ksz - i)) > 0) + i += w; + + if (w < 0) + return -errno; + return i != ksz; +} + diff --git a/db.h b/db.h @@ -0,0 +1,31 @@ +#ifndef DB_H +#define DB_H + +#include <time.h> +#include <limits.h> +#include "token.h" + +struct AES_ctx; + +struct totpkey { + uint8_t t0[sizeof(uint64_t)]; + uint8_t digest; + uint8_t digits; + uint8_t period; + uint8_t keylen; + uint8_t desclen; + uint8_t issuerlen; + uint8_t filler1; + uint8_t filler2; +}; + +int db_open_read(const char *filename, struct AES_ctx *c); +int db_open_write(const char *filename, struct AES_ctx *c); +void db_foreach(int fd, struct AES_ctx *c, + void (*key_cb)(struct token *token, + void *data), + void *cb_data); +int db_add_key(int fd, struct AES_ctx *c, + struct token *token); + +#endif diff --git a/main.c b/main.c @@ -12,7 +12,6 @@ #include <time.h> #include <unistd.h> -#include <arpa/inet.h> #include <sys/stat.h> #include "sha1.h" @@ -21,6 +20,8 @@ #include "tiny-AES-c/aes.h" #include "arg.h" #include "util.h" +#include "db.h" +#include "token.h" #define SECRET_DB_PATH ".local/share/totp" #define SECRET_DB_FILE "secrets.db" @@ -28,22 +29,6 @@ char *argv0; -enum digest { - DIGEST_SHA1 = 0, - DIGEST_SHA224, - DIGEST_SHA256, - DIGEST_SHA384, - DIGEST_SHA512, -}; - -static const char *digest_names[] = { - "SHA1", - "SHA224", - "SHA256", - "SHA384", - "SHA512", -}; - static void (*digest_hmacs[])(const void *key, size_t keylen, const void *data, size_t datalen, void *h) = { @@ -62,23 +47,11 @@ static size_t digest_sizes[] = { SHA512_HASHSIZE, }; -uint8_t get_digest(const char *s, size_t len) -{ - size_t i; - - for (i = 0; i < sizeof(digest_names) / sizeof(*digest_names); i++) - if (!strncmp(s, digest_names[i], len) && - !digest_names[i][len]) - return i; - - fprintf(stderr, "Unknown digest \"%.*s\", assuming %s\n", - (int)len, s, digest_names[DIGEST_SHA1]); - return DIGEST_SHA1; -} - -void print_base32(const uint8_t *buffer, size_t len) +static void print_base32(FILE *stream, struct bytes data) { const char *chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + uint8_t *buffer = data.data; + size_t len = data.len; uint16_t v = 0; size_t b = 0; @@ -86,248 +59,64 @@ void print_base32(const uint8_t *buffer, size_t len) v = v << 8 | *buffer++; b += 8; while (b >= 5) { - printf("%c", chars[(v >> (b - 5)) & 31]); + fprintf(stream, "%c", chars[(v >> (b - 5)) & 31]); b -= 5; } } if (b) - printf("%c", chars[(v << (5 - b)) & 31]); -} - -char *_if_prefix(char *s, const char *prefix, size_t prefixlen) -{ - if (strncmp(s, prefix, prefixlen)) - return NULL; - return s + prefixlen; -} - -#define if_prefix(s, p) _if_prefix(s, p, sizeof(p) - 1) - -struct header { - uint8_t magic[4]; - uint8_t version; -}; - -bool verify_db(int fd, struct AES_ctx *c) -{ - uint8_t rbuf[AES_BLOCKLEN]; - int r; - size_t rused = 0; - struct header *h; - - while ((r = read(fd, rbuf + rused, sizeof(rbuf) - rused)) > 0) - rused += r; - - if (rused < sizeof(rbuf)) - return false; - - AES_CBC_decrypt_buffer(c, rbuf, sizeof(rbuf)); - h = (struct header *)(rbuf + rbuf[0] % (sizeof(rbuf) - sizeof(*h) - 1) + 1); - - if (h->magic[0] != 'T' || - h->magic[1] != 'O' || - h->magic[2] != 'T' || - h->magic[3] != 'P' || - h->version != 1) - croak("Secret database decryption failed, check passphrase"); - - return true; + fprintf(stream, "%c", chars[(v << (5 - b)) & 31]); } -void write_header(int fd, struct AES_ctx *c) +void print_key(struct token *token, void *data) { - uint8_t wbuf[AES_BLOCKLEN]; - int w; - size_t written = 0; - size_t i; - struct header *h; - - for (i = 0; i < sizeof(wbuf); i++) - wbuf[i] = rand(); - - h = (struct header *)(wbuf + wbuf[0] % (sizeof(wbuf) - sizeof(*h) - 1) + 1); - - h->magic[0] = 'T'; - h->magic[1] = 'O'; - h->magic[2] = 'T'; - h->magic[3] = 'P'; - h->version = 1; - - AES_CBC_encrypt_buffer(c, wbuf, sizeof(wbuf)); - - while (written < sizeof(wbuf) && (w = write(fd, wbuf + written, sizeof(wbuf) - written)) > 0) - written += w; -} - -struct totpkey { - uint64_t t0; - uint8_t digest; - uint8_t digits; - uint8_t period; - uint8_t keylen; - uint8_t desclen; - uint8_t issuerlen; - uint8_t filler1; - uint8_t filler2; -}; - -void read_keys(int fd, struct AES_ctx *c, - void (*key_cb)(uint8_t digest, - uint8_t digits, - uint8_t period, - time_t t0, - const uint8_t *key, size_t keylen, - const char *desc, size_t desclen, - const char *issuer, size_t issuerlen, - void *data), - void *cb_data) -{ - uint8_t decbuf[512]; - uint8_t rbuf[AES_BLOCKLEN]; - size_t dused = 0; - size_t rused = 0; - int r; - - while ((r = read(fd, rbuf + rused, sizeof(rbuf) - rused)) > 0) { - struct totpkey *kh = (struct totpkey *)decbuf; - if ((rused += r) < sizeof(rbuf)) - continue; - AES_CBC_decrypt_buffer(c, rbuf, sizeof(rbuf)); - if (dused + sizeof(rbuf) >= sizeof(decbuf)) - break; - memcpy(decbuf + dused, rbuf, sizeof(rbuf)); - rused = 0; - dused += sizeof(rbuf); - - if (dused < sizeof(*kh) + - kh->keylen + kh->desclen + kh->issuerlen) - continue; - - key_cb(kh->digest, - kh->digits, - kh->period, - _ntohll(kh->t0), - decbuf + sizeof(*kh), kh->keylen, - (const char *)(decbuf + sizeof(*kh) + kh->keylen), - kh->desclen, - (const char *)(decbuf + sizeof(*kh) + kh->keylen + kh->desclen), - kh->issuerlen, - cb_data); - dused = 0; - } -} - -int write_key(int fd, struct AES_ctx *c, - uint8_t digest, - uint8_t digits, - uint8_t period, - time_t t0, - const uint8_t *key, size_t keylen, - const char *desc, size_t desclen, - const char *issuer, size_t issuerlen) -{ - size_t ksz = sizeof(struct totpkey) + keylen + desclen + issuerlen; - size_t i; - int w; - - ksz += AES_BLOCKLEN - 1 - ((ksz - 1) % AES_BLOCKLEN); - - if (keylen > UINT8_MAX || desclen > UINT8_MAX) - return -EINVAL; - - uint8_t buffer[ksz]; - memcpy(buffer, &(struct totpkey){ - .t0 = _htonll(t0), - .digest = digest, - .digits = digits, - .period = period, - .keylen = keylen, - .desclen = desclen, - .issuerlen = issuerlen }, sizeof(struct totpkey)); - memcpy(buffer + sizeof(struct totpkey), key, keylen); - memcpy(buffer + sizeof(struct totpkey) + keylen, desc, desclen); - memcpy(buffer + sizeof(struct totpkey) + keylen + desclen, issuer, issuerlen); - memset(buffer + sizeof(struct totpkey) + keylen + desclen + issuerlen, 0, - ksz - sizeof(struct totpkey) - keylen - desclen - issuerlen); - - for (i = 0; i < ksz; i += AES_BLOCKLEN) - AES_CBC_encrypt_buffer(c, buffer + i, AES_BLOCKLEN); - i = 0; - - while ((w = write(fd, buffer + i, ksz - i)) > 0) - i += w; - - if (w < 0) - return -errno; - return i != ksz; -} - -void print_key(uint8_t digest, - uint8_t digits, - uint8_t period, - time_t t0, - const uint8_t *key, size_t keylen, - const char *desc, size_t desclen, - const char *issuer, size_t issuerlen, - void *data) -{ - (void)digest; - (void)digits; - (void)period; - (void)key; - (void)keylen; - (void)issuer; - (void)issuerlen; - (void)t0; + FILE *stream = data; (void)data; - printf("%.*s by %.*s\n", (int)desclen, desc, (int)issuerlen, issuer); + fprintf(stream, "%.*s by %.*s\n", (int)token->desc.len, token->desc.data, (int)token->issuer.len, token->issuer.data); } -static void print_uriencode(const char *buf, size_t len, bool getarg) +static void print_uriencode(FILE *stream, struct bytes data, bool getarg) { const char *escape = ":/@+% &?"; - while (len && *buf) { - size_t pass = strncspn(buf, len, escape); + const char *buf = (const char *)data.data; + const char *end = buf + data.len; + while (buf < end && *buf) { + size_t pass = strncspn(buf, end - buf, escape); printf("%.*s", (int)pass, buf); buf += pass; - len -= pass; - while (len && *buf && strchr(escape, *buf)) { + while (buf < end && *buf && strchr(escape, *buf)) { if (*buf == ' ' && getarg) - printf("+"); + fprintf(stream, "+"); else - printf("%%%02" PRIx8, *(uint8_t *)buf); + fprintf(stream, "%%%02" PRIx8, *(uint8_t *)buf); buf++; - len--; } } } -void print_keyuri(uint8_t digest, - uint8_t digits, - uint8_t period, - time_t t0, - const uint8_t *key, size_t keylen, - const char *desc, size_t desclen, - const char *issuer, size_t issuerlen, +void print_keyuri(struct token *token, void *data) { - (void)t0; - (void)data; - printf("otpauth://totp/"); - print_uriencode(desc, desclen, false); - printf("?secret="); - print_base32(key, keylen); - if (issuerlen) { - printf("&issuer="); - print_uriencode(issuer, issuerlen, true); + FILE *stream = data; + + fputs("otpauth://totp/", stream); + if (token->issuer.len) { + print_uriencode(stream, token->issuer, false); + fputc(':', stream); + } + print_uriencode(stream, token->desc, false); + fputs("?secret=", stream); + print_base32(stream, token->key); + if (token->issuer.len) { + fputs("&issuer=", stream); + print_uriencode(stream, token->issuer, true); } - printf("&algorithm=%s&digits=%" PRIu8 "&period=%" PRIu8 "\n", - digest_names[digest], - digits, - period); + fprintf(stream, "&algorithm=%s&digits=%" PRIu8 "&period=%" PRIu8 "\n", + digest_names[token->digest], + token->digits, + token->period); } struct generate_data { @@ -335,35 +124,30 @@ struct generate_data { bool found; }; -void generate_token(uint8_t digest, - uint8_t digits, - uint8_t period, - time_t t0, - const uint8_t *key, size_t keylen, - const char *desc, size_t desclen, - const char *issuer, size_t issuerlen, - void *data) +void generate_token(struct token *token, void *data) { struct generate_data *d = data; uint32_t modulo = 1; uint8_t i; - char descbuf[desclen + 1]; + char descbuf[512]; + char *dp = descbuf; - (void)issuer; - (void)issuerlen; - - memcpy(descbuf, desc, desclen); - descbuf[desclen] = '\0'; + if (token->issuer.len) { + dp = mempush(dp, token->issuer.data, token->issuer.len); + *dp++ = ':'; + } + dp = mempush(dp, token->desc.data, token->desc.len); + *dp = '\0'; if (fnmatch(d->filter, descbuf, FNM_NOESCAPE)) return; d->found = true; - for (i = 0; i < digits; i++) + for (i = 0; i < token->digits; i++) modulo *= 10; - printf("%0*" PRIu32 "\n", (int)digits, - totp(key, keylen, time(NULL), period, t0, digest_hmacs[digest], digest_sizes[digest]) % modulo); + printf("%0*" PRIu32 "\n", (int)token->digits, + totp(token->key.data, token->key.len, time(NULL), token->period, token->t0, digest_hmacs[token->digest], digest_sizes[token->digest]) % modulo); } struct write_filter_data { @@ -372,31 +156,22 @@ struct write_filter_data { struct AES_ctx *c; }; -void write_filter_key(uint8_t digest, - uint8_t digits, - uint8_t period, - time_t t0, - const uint8_t *key, size_t keylen, - const char *desc, size_t desclen, - const char *issuer, size_t issuerlen, +void write_filter_key(struct token *token, void *data) { struct write_filter_data *d = data; if (d->filter) { - char descbuf[desclen + 1]; + char descbuf[UINT8_MAX + 1]; - memcpy(descbuf, desc, desclen); - descbuf[desclen] = '\0'; + memcpy(descbuf, token->desc.data, token->desc.len); + descbuf[token->desc.len] = '\0'; if (!fnmatch(d->filter, descbuf, FNM_NOESCAPE)) return; } - write_key(d->fd, d->c, digest, digits, period, t0, - key, keylen, - desc, desclen, - issuer, issuerlen); + db_add_key(d->fd, d->c, token); } enum cmd { @@ -412,50 +187,18 @@ void usage() { fprintf(stderr, "Usage: totp [OPTIONS]\n" + "-f <file>\tuse file as database\n" "-k <pass>\tpassphrase for database encryption\n" "-K <file>\tread encryption passphrase from file\n" "-l\tlist known secrets\n" "-a <uri>\tadd uri to secrets\n" "-d <filter>\tremove secrets matching filter\n" "-t <filter>\tgenerate tokens for secrets matching filter\n" + "-T <time>\toverride current time for token generation\n" "-e\texport secrets\n"); exit(1); } -static inline char dehex(const char *s) -{ - if ((*s < '0' || - (*s > '9' && (*s & ~0x20) < 'A') || - (*s & ~0x20) > 'F') || - (s[1] < '0' || - (s[1] > '9' && (s[1] & ~0x20) < 'A') || - (s[1] & ~0x20) > 'F')) - return '?'; - return (*s < 'A' ? *s - '0' : (*s & ~0x20) - 'A' + 10) << 4 | - (s[1] < 'A' ? s[1] - '0' : (s[1] & ~0x20) - 'A' + 10); -} - -static size_t uridecode(char *buf, size_t len, bool getarg) -{ - char *w = buf; - const char *r = buf; - - while (r - buf < (ptrdiff_t)len) { - if (*r == '%') { - if (r - buf + 2 >= (ptrdiff_t)len) - break; - *w++ = dehex(++r); - r += 2; - } else if (getarg && *r == '+') { - *w++ = ' '; - r++; - } else - *w++ = *r++; - } - - return w - buf; -} - static void setecho(bool echo) { struct termios tio; @@ -609,82 +352,34 @@ int main(int argc, char *argv[]) switch (cmd) { case CMD_LIST: - free(newsecretfile); - fd = open(secretfile, O_RDONLY); - if (free_secretfile) - free(secretfile); + fd = db_open_read(secretfile, &c); if (fd < 0) break; - if (!verify_db(fd, &c)) - croak("Unable to open database, check passphrase"); - read_keys(fd, &c, print_key, NULL); + db_foreach(fd, &c, print_key, stdout); close(fd); break; case CMD_ADD: { - size_t kl = 0; - size_t dl = 0; - char *i; - char *key; - char *desc; - uint8_t digest = DIGEST_SHA1; - uint8_t digits = 6; - uint8_t period = 30; - uint8_t issuerlen = 0; - time_t t0 = 0; - char *issuer; - - if (!(desc = if_prefix(totpuri, "otpauth://totp/"))) - usage(); - - i = strchr(desc, '?'); - if (!i) - usage(); - - dl = uridecode(desc, i - desc, false); - - while (*i++) { - char *v; - if ((v = if_prefix(i, "secret="))) { - i = v + strcspn(v, "&"); - kl = debase32(key = v, i - v); - } else if ((v = if_prefix(i, "digits="))) { - digits = strtoul(v, &i, 10); - } else if ((v = if_prefix(i, "period="))) { - period = strtoul(v, &i, 10); - } else if ((v = if_prefix(i, "issuer="))) { - i = v + strcspn(v, "&"); - issuerlen = uridecode(issuer = v, i - v, true); - } else if ((v = if_prefix(i, "algorithm="))) { - i = v + strcspn(v, "&"); - digest = get_digest(v, i - v); - } else { - i += strcspn(i, "&"); - } - } + struct token token = token_parse_uri(totpuri); + if (!token.valid) + croak("Invalid uri"); - fd = open(secretfile, O_RDONLY, 0600); - if (fd >= 0) - verify_db(fd, &c); + fd = db_open_read(secretfile, &c); + if (fd < 0 && errno != ENOENT) + croak("Opening existing db failed: %s", strerror(errno)); - wfd = open(newsecretfile, - O_WRONLY | O_TRUNC | O_CREAT, 0600); - write_header(wfd, &wc); + wfd = db_open_write(newsecretfile, &wc); + if (wfd < 0) + croak("Could not open temporary secret file: %s", strerror(errno)); if (fd >= 0) { - read_keys(fd, &c, write_filter_key, - &(struct write_filter_data){ - .fd = wfd, .c = &wc }); + db_foreach(fd, &c, write_filter_key, + &(struct write_filter_data){ .fd = wfd, .c = &wc }); close(fd); } - write_key(wfd, &wc, - digest, digits, period, t0, - (uint8_t *)key, kl, desc, dl, - issuer, issuerlen); + db_add_key(wfd, &wc, &token); close(wfd); rename(newsecretfile, secretfile); - free(newsecretfile); - free(secretfile); break; } @@ -693,51 +388,49 @@ int main(int argc, char *argv[]) keyquery = argv[0]; /* fall-through */ case CMD_TOK: - free(newsecretfile); - fd = open(secretfile, O_RDONLY); - free(secretfile); - if (fd >= 0) { - verify_db(fd, &c); - gd.filter = keyquery; - read_keys(fd, &c, generate_token, &gd); - close(fd); - } + fd = db_open_read(secretfile, &c); + if (fd < 0) + croak("Could not open secret file: %s", strerror(errno)); + gd.filter = keyquery; + db_foreach(fd, &c, generate_token, &gd); + close(fd); if (!gd.found) croak("No secrets matching filter found"); break; case CMD_DEL: { - fd = open(secretfile, O_RDONLY); - if (fd < 0) - exit(1); - wfd = open(newsecretfile, - O_WRONLY | O_TRUNC | O_CREAT, 0600); - verify_db(fd, &c); - write_header(wfd, &wc); - read_keys(fd, &c, write_filter_key, - &(struct write_filter_data){ - .fd = wfd, .filter = keyquery, - .c = &wc - }); + fd = db_open_read(secretfile, &c); + if (fd < 0) { + if (errno == ENOENT) + break; + croak("Could not open secret file: %s", strerror(errno)); + } + wfd = db_open_write(newsecretfile, &wc); + if (wfd < 0) + croak("Could not open temporary secret file: %s", strerror(errno)); + db_foreach(fd, &c, write_filter_key, + &(struct write_filter_data){ + .fd = wfd, .filter = keyquery, + .c = &wc + }); close(wfd); close(fd); rename(newsecretfile, secretfile); - free(newsecretfile); - free(secretfile); break; case CMD_EXP: - free(newsecretfile); - fd = open(secretfile, O_RDONLY); - free(secretfile); + fd = db_open_read(secretfile, &c); if (fd < 0) - break; - verify_db(fd, &c); - read_keys(fd, &c, print_keyuri, NULL); + croak("Could not open secret file: %s", strerror(errno)); + db_foreach(fd, &c, print_keyuri, stdout); close(fd); break; } } + if (free_secretfile) + free(secretfile); + free(newsecretfile); + return 0; } diff --git a/sha512.c b/sha512.c @@ -68,7 +68,7 @@ static inline void _sha512_update(uint64_t *h, const void *data) 0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, 0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817 }; - const uint64_t *d = data; + const uint8_t *d = data; uint64_t w[16]; size_t i; @@ -76,8 +76,10 @@ static inline void _sha512_update(uint64_t *h, const void *data) uint64_t wr[8]; memcpy(wr, h, sizeof(wr)); - for (i = 0; i < sizeof(w) / sizeof(*w); i++) - rotmod8(wr, k[i], w[i] = _ntohll(d[i])); + for (i = 0; i < sizeof(w) / sizeof(*w); i++) { + w[i] = readbeu64(&d[i * sizeof(uint64_t)]); + rotmod8(wr, k[i], w[i]); + } for (; i < sizeof(k) / sizeof(*k); i++) rotmod8(wr, k[i], getnw(w, i)); @@ -121,11 +123,11 @@ void sha512_finish(struct sha512 *s) } else { memset(s->buffer + (s->len & 127) + 1, 0, 119 - (s->len & 127)); } - ((uint64_t *)s->buffer)[15] = _htonll(s->len << 3); + writebeu64(&s->buffer[120], s->len << 3); _sha512_update(s->h, s->buffer); for (i = 0; i < sizeof(s->h) / sizeof(*s->h); i++) - s->h[i] = _htonll(s->h[i]); + writebeu64(&s->h[i], s->h[i]); } void sha384_init(struct sha384 *s) diff --git a/token.c b/token.c @@ -0,0 +1,142 @@ +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <stdio.h> +#include "token.h" +#include "util.h" + +const char *digest_names[] = { + "SHA1", + "SHA224", + "SHA256", + "SHA384", + "SHA512", +}; + +static inline char dehex(const char *s) +{ + const uint8_t *u = (const uint8_t *)s; + static const uint8_t vals[256] = { + ['0'] = 1, ['1'] = 2, ['2'] = 3, ['3'] = 4, ['4'] = 5, + ['5'] = 6, ['6'] = 7, ['7'] = 8, ['8'] = 9, ['9'] = 10, + ['A'] = 11, ['B'] = 12, ['C'] = 13, ['D'] = 14, ['E'] = 15, + ['F'] = 16, ['a'] = 11, ['b'] = 12, ['c'] = 13, ['d'] = 14, + ['e'] = 15, ['f'] = 16, + }; + + if (!vals[u[0]] || !vals[u[1]]) + return 0; + + return (vals[u[0]] - 1) << 4 | + (vals[u[1]] - 1); +} + +static struct bytes uridecode(struct bytes data, bool getarg) +{ + char *w = (char *)data.data; + char *end = w + data.len; + const char *r = w; + + while (r < end) { + if (*r == '%') { + if (r + 2 >= end) + return (struct bytes){ 0 }; + *w++ = dehex(++r); + if (!w[-1]) + return (struct bytes){ 0 }; + r += 2; + } else if (getarg && *r == '+') { + *w++ = ' '; + r++; + } else + *w++ = *r++; + } + + return (struct bytes){ data.data, w - (char *)data.data }; +} + + +static uint8_t get_digest(const char *s, size_t len) +{ + size_t i; + + for (i = 0; i < sizeof(digest_names) / sizeof(*digest_names); i++) + if (!strncmp(s, digest_names[i], len) && + !digest_names[i][len]) + return i; + + fprintf(stderr, "Unknown digest \"%.*s\", assuming %s\n", + (int)len, s, digest_names[DIGEST_SHA1]); + return DIGEST_SHA1; +} + +static uint8_t strtou8(const char *s, char **end) { + unsigned long v = strtoul(s, end, 10); + if (v > UINT8_MAX || v == 0) { + errno = ERANGE; + return 0; + } + return v; +} + +struct token token_parse_uri(char *data) { + struct token rv = { .t0 = 0, .period = 60, .digits = 6, .digest = DIGEST_SHA1, .valid = false }; + char *str; + char *i; + char *v; + + if (!(str = if_prefix(data, "otpauth://totp/"))) + return rv; + + i = strchr(str, '?'); + if (!i) + return rv; + + rv.desc = uridecode((struct bytes){ (uint8_t *)str, i - str }, false); + + if ((v = memchr(rv.desc.data, ':', rv.desc.len))) { + rv.issuer = (struct bytes){ rv.desc.data, v++ - (char *)rv.desc.data }; + rv.desc = (struct bytes){ (void *)v, i - v }; + } + + while (*i++) { + if ((v = if_prefix(i, "secret="))) { + if (rv.key.len) + croak("Multiple secrets in URI"); + i = v + strcspn(v, "&"); + rv.key = debase32(v, i - v); + } else if ((v = if_prefix(i, "digits="))) { + if (!(rv.digits = strtou8(v, &i))) + return rv; + } else if ((v = if_prefix(i, "period="))) { + if (!(rv.period = strtou8(v, &i))) + return rv; + } else if ((v = if_prefix(i, "issuer="))) { + i = v + strcspn(v, "&"); + struct bytes newiss = uridecode((struct bytes){ (void *)v, i - v }, true); + if (rv.issuer.len && (newiss.len != rv.issuer.len || + memcmp(rv.issuer.data, newiss.data, newiss.len))) { + errno = EINVAL; + return rv; + } + rv.issuer = newiss; + } else if ((v = if_prefix(i, "algorithm="))) { + i = v + strcspn(v, "&"); + rv.digest = get_digest(v, i - v); + } else { + i += strcspn(i, "&"); + } + + if (!i && *i != '&') { + errno = EINVAL; + return rv; + } + } + + if (rv.key.len && rv.desc.len) + rv.valid = true; + else + errno = EINVAL; + + return rv; +} diff --git a/token.h b/token.h @@ -0,0 +1,33 @@ +#ifndef TOKEN_H +#define TOKEN_H + +#include <stdlib.h> +#include <stdbool.h> +#include <stdint.h> +#include <time.h> +#include "util.h" + +enum digest { + DIGEST_SHA1 = 0, + DIGEST_SHA224, + DIGEST_SHA256, + DIGEST_SHA384, + DIGEST_SHA512, +}; + +extern const char *digest_names[]; + +struct token { + struct bytes key; + struct bytes desc; + struct bytes issuer; + enum digest digest; + time_t t0; + uint8_t digits; + uint8_t period; + bool valid; +}; + +struct token token_parse_uri(char *data); + +#endif /* TOKEN_H */ diff --git a/util.c b/util.c @@ -63,11 +63,15 @@ uint32_t hotp(const void *key, size_t keylen, void *h), size_t hshsz) { uint8_t h[hshsz]; + uint8_t *pos; hmac_f(key, keylen, counter, counterlen, h); + pos = &h[h[hshsz - 1] & 0xf]; - return ntohl(*(uint32_t *)&((uint8_t *)h)[h[hshsz - 1] & 0xf]) & - 0x7fffffff; + return (uint32_t)(pos[0] & INT8_MAX) << 24 | + (uint32_t)pos[1] << 16 | + (uint32_t)pos[2] << 8 | + pos[3]; } uint32_t totp(const void *key, size_t keylen, @@ -77,9 +81,10 @@ uint32_t totp(const void *key, size_t keylen, const void *data, size_t datalen, void *h), size_t hshsz) { - uint64_t tv = _htonll((t1 - t0) / period); + uint8_t tb[sizeof(uint64_t)]; + writebeu64(tb, (t1 - t0) / period); - return hotp(key, keylen, &tv, sizeof(tv), hmac_f, hshsz); + return hotp(key, keylen, &tb, sizeof(tb), hmac_f, hshsz); } size_t strncspn(const char *s, size_t l, const char *c) @@ -128,21 +133,47 @@ void croak(const char *fmt, ...) exit(1); } -size_t debase32(char *buffer, size_t len) +struct bytes debase32(char *buffer, size_t len) { uint8_t *wp = (uint8_t *)buffer; const uint8_t *rp = (const uint8_t *)buffer; uint16_t v = 0; size_t b = 0; + static const uint8_t val[256] = { + ['A'] = 1, ['B'] = 2, ['C'] = 3, ['D'] = 4, ['E'] = 5, + ['F'] = 6, ['G'] = 7, ['H'] = 8, ['I'] = 9, ['J'] = 10, + ['K'] = 11, ['L'] = 12, ['M'] = 13, ['N'] = 14, ['O'] = 15, + ['P'] = 16, ['Q'] = 17, ['R'] = 18, ['S'] = 19, ['T'] = 20, + ['U'] = 21, ['V'] = 22, ['W'] = 23, ['X'] = 24, ['Y'] = 25, + ['Z'] = 26, ['a'] = 1, ['b'] = 2, ['c'] = 3, ['d'] = 4, + ['e'] = 5, ['f'] = 6, ['g'] = 7, ['h'] = 8, ['i'] = 9, + ['j'] = 10, ['k'] = 11, ['l'] = 12, ['m'] = 13, ['n'] = 14, + ['o'] = 15, ['p'] = 16, ['q'] = 17, ['r'] = 18, ['s'] = 19, + ['t'] = 20, ['u'] = 21, ['v'] = 22, ['w'] = 23, ['x'] = 24, + ['y'] = 25, ['z'] = 26, ['2'] = 27, ['3'] = 28, ['4'] = 29, + ['5'] = 30, ['6'] = 31, ['7'] = 32 + }; + for (rp = (uint8_t *)buffer; (char *)rp - buffer < (ptrdiff_t)len && *rp && *rp != '='; rp++) { - uint8_t c = *rp >= 'a' ? *rp - 'a' : *rp >= 'A' ? *rp - 'A' : *rp - '2' + 26; - v = v << 5 | c; + uint8_t c = val[*rp]; + if (!c) + return (struct bytes){ 0 }; + v = v << 5 | (c - 1); b += 5; if (b >= 8) { *wp++ = (v >> (b & 7)) & 255; b -= 8; } } - return (char *)wp - buffer; + return (struct bytes){ (void *)buffer, (char *)wp - buffer }; +} + +void randmem(void *mem, size_t n) +{ + uint8_t *wp = mem; + uint8_t *end = wp + n; + + while (wp < end) + *wp++ = rand(); } diff --git a/util.h b/util.h @@ -4,15 +4,20 @@ #include <stdlib.h> #include <stddef.h> #include <stdint.h> +#include <string.h> #include <time.h> -#include <arpa/inet.h> +struct bytes { + uint8_t *data; + size_t len; +}; typedef void (*digest_init)(void *c); typedef void (*digest_update)(void *c, const void *data, size_t len); typedef void (*digest_finish)(void *c); void xormem(void *a, const void *b, size_t len); +void randmem(void *a, size_t len); void hmac(const void *key, size_t keylen, const void *data, size_t datalen, digest_init init, @@ -40,31 +45,9 @@ uint32_t hotp(const void *key, size_t keylen, size_t strncspn(const char *haystack, size_t haystacklen, const char *needles); -static inline uint64_t _htonll(uint64_t v) -{ - union { - uint64_t v64; - uint32_t v32[2]; - } rv; - rv.v32[0] = htonl(v >> 32); - rv.v32[1] = htonl(v & 0xffffffffU); - - return rv.v64; -} - -static inline uint64_t _ntohll(uint64_t v) -{ - union { - uint64_t v64; - uint32_t v32[2]; - } rv; - rv.v64 = v; - - return (uint64_t)ntohl(rv.v32[0]) << 32 | ntohl(rv.v32[1]); -} - -static inline void writebeu64(uint8_t *buffer, uint64_t v) +static inline void *writebeu64(void *dest, uint64_t v) { + uint8_t *buffer = dest; *buffer++ = v >> 56; *buffer++ = v >> 48; *buffer++ = v >> 40; @@ -73,9 +56,43 @@ static inline void writebeu64(uint8_t *buffer, uint64_t v) *buffer++ = v >> 16; *buffer++ = v >> 8; *buffer++ = v; + return buffer; +} + +static inline uint64_t readbeu64(const void *src) +{ + const uint8_t *buffer = src; + return (uint64_t)buffer[0] << 56 | + (uint64_t)buffer[1] << 48 | + (uint64_t)buffer[2] << 40 | + (uint64_t)buffer[3] << 32 | + (uint64_t)buffer[4] << 24 | + (uint64_t)buffer[5] << 16 | + (uint64_t)buffer[6] << 8 | + (uint64_t)buffer[7]; +} + +static inline void *mempush(void *dst, const void *src, size_t n) +{ + if (n) + memcpy(dst, src, n); + return (char *)dst + n; +} + +static inline void *mempushb(void *dst, struct bytes data) +{ + return mempush(dst, data.data, data.len); +} + +static inline char *if_prefix(const char *s, const char *prefix) +{ + while (*prefix) + if (*s++ != *prefix++) + return NULL; + return (char *)s; } void croak(const char *fmt, ...); -size_t debase32(char *buffer, size_t len); +struct bytes debase32(char *buffer, size_t len); #endif