Last active
November 24, 2025 20:13
-
-
Save laura240406/3ac44a50901e85e2b48de99fb232391f to your computer and use it in GitHub Desktop.
Really simple dependency-free TOTP cli tool. Reads base32 secret from $HOME/.config/totp and prints the TOTP to stdout.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <ctype.h> | |
| #include <errno.h> | |
| #include <stdint.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/mman.h> | |
| #include <time.h> | |
| #include <unistd.h> | |
| static inline int b32_val(char c) { | |
| if (c >= 'A' && c <= 'Z') | |
| return c - 'A'; | |
| if (c >= '2' && c <= '7') | |
| return c - '2' + 26; | |
| return -1; | |
| } | |
| static size_t base32_decode(char *input, uint8_t *output) { | |
| uint32_t buffer = 0; | |
| int bits = 0; | |
| size_t ptr = 0; | |
| for (; *input; input++) { | |
| int c = b32_val(*input); | |
| if (c == -1) | |
| continue; | |
| buffer = (buffer << 5) | c; | |
| bits += 5; | |
| if (bits >= 8) { | |
| bits -= 8; | |
| output[ptr] = (buffer >> bits) & 0xff; | |
| ptr++; | |
| } | |
| } | |
| return ptr; | |
| } | |
| static inline uint32_t r(uint32_t x, int n) { return (x << n) | (x >> (32 - n)); } | |
| static void pb(const uint8_t b[64], uint32_t s[5]) { | |
| uint32_t w[80], a = s[0], bb = s[1], c = s[2], d = s[3], e = s[4]; | |
| for (int i = 0; i < 16; i++) { | |
| int j = i * 4; | |
| w[i] = ((uint32_t) b[j] << 24) | ((uint32_t) b[j + 1] << 16) | ((uint32_t) b[j + 2] << 8) | b[j + 3]; | |
| } | |
| for (int i = 16; i < 80; i++) | |
| w[i] = r(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); | |
| for (int i = 0; i < 80; i++) { | |
| uint32_t f, k; | |
| if (i < 20) { | |
| f = (bb & c) | ((~bb) & d); | |
| k = 0x5a827999u; | |
| } else if (i < 40) { | |
| f = bb ^ c ^ d; | |
| k = 0x6ed9eba1u; | |
| } else if (i < 60) { | |
| f = (bb & c) | (bb & d) | (c & d); | |
| k = 0x8f1bbcdcu; | |
| } else { | |
| f = bb ^ c ^ d; | |
| k = 0xca62c1d6u; | |
| } | |
| uint32_t t = r(a, 5) + f + e + k + w[i]; | |
| e = d; | |
| d = c; | |
| c = r(bb, 30); | |
| bb = a; | |
| a = t; | |
| } | |
| s[0] += a; | |
| s[1] += bb; | |
| s[2] += c; | |
| s[3] += d; | |
| s[4] += e; | |
| } | |
| static void sha1_two(const uint8_t *a, size_t al, const uint8_t *b, size_t bl, uint8_t out[20]) { | |
| uint32_t s[5] = {0x67452301u, 0xefcdab89u, 0x98badcfeu, 0x10325476u, 0xc3d2e1f0u}; | |
| uint8_t tmp[64]; | |
| size_t rem = 0; | |
| const uint8_t *ptr; | |
| size_t len; | |
| ptr = a; | |
| len = al; | |
| while (len >= 64) { | |
| pb(ptr, s); | |
| ptr += 64; | |
| len -= 64; | |
| } | |
| if (len) { | |
| memcpy(tmp, ptr, len); | |
| rem = len; | |
| } | |
| ptr = b; | |
| len = bl; | |
| if (rem) { | |
| size_t need = 64 - rem; | |
| if (len >= need) { | |
| memcpy(tmp + rem, ptr, need); | |
| pb(tmp, s); | |
| ptr += need; | |
| len -= need; | |
| rem = 0; | |
| } else { | |
| memcpy(tmp + rem, ptr, len); | |
| rem += len; | |
| ptr += len; | |
| len = 0; | |
| } | |
| } | |
| while (len >= 64) { | |
| pb(ptr, s); | |
| ptr += 64; | |
| len -= 64; | |
| } | |
| if (len) { | |
| memcpy(tmp + rem, ptr, len); | |
| rem += len; | |
| } | |
| tmp[rem] = 0x80; | |
| for (size_t i = rem + 1; i < 64; i++) | |
| tmp[i] = 0; | |
| if (rem + 1 > 56) { | |
| pb(tmp, s); | |
| for (size_t i = 0; i < 56; i++) | |
| tmp[i] = 0; | |
| } | |
| uint64_t bits = ((uint64_t) al + (uint64_t) bl) * 8ULL; | |
| tmp[56] = (bits >> 56) & 0xff; | |
| tmp[57] = (bits >> 48) & 0xff; | |
| tmp[58] = (bits >> 40) & 0xff; | |
| tmp[59] = (bits >> 32) & 0xff; | |
| tmp[60] = (bits >> 24) & 0xff; | |
| tmp[61] = (bits >> 16) & 0xff; | |
| tmp[62] = (bits >> 8) & 0xff; | |
| tmp[63] = bits & 0xff; | |
| pb(tmp, s); | |
| for (int i = 0; i < 5; i++) { | |
| out[i * 4] = (s[i] >> 24) & 0xff; | |
| out[i * 4 + 1] = (s[i] >> 16) & 0xff; | |
| out[i * 4 + 2] = (s[i] >> 8) & 0xff; | |
| out[i * 4 + 3] = s[i] & 0xff; | |
| } | |
| } | |
| static void sha1(const uint8_t *d, size_t l, uint8_t out[20]) { sha1_two(d, l, 0, 0, out); } | |
| static void hmac_sha1(const uint8_t *k, size_t kl, const uint8_t *d, size_t dl, uint8_t o[20]) { | |
| uint8_t k0[64], ip[64], op[64], ih[20], tmp[64 + 20]; | |
| memset(k0, 0, 64); | |
| if (kl > 64) | |
| sha1(k, kl, k0); | |
| else if (kl) | |
| memcpy(k0, k, kl); | |
| for (int i = 0; i < 64; i++) { | |
| ip[i] = k0[i] ^ 0x36u; | |
| op[i] = k0[i] ^ 0x5cu; | |
| } | |
| sha1_two(ip, 64, d, dl, ih); | |
| memcpy(tmp, op, 64); | |
| memcpy(tmp + 64, ih, 20); | |
| sha1(tmp, 64 + 20, o); | |
| } | |
| static uint32_t totp(char *ssecret) { | |
| // https://drewdevault.com/2022/10/18/TOTP-is-easy.html | |
| uint64_t tm = time(NULL) / 30; | |
| int t = (strlen(ssecret) * 5 + 4) / 8; | |
| uint8_t *secret = (uint8_t *) malloc(t); | |
| memset(secret, 0, t); | |
| mlock(secret, t); | |
| size_t slen = base32_decode(ssecret, secret); | |
| uint8_t b[8]; | |
| b[0] = (tm >> 56) & 0xff; | |
| b[1] = (tm >> 48) & 0xff; | |
| b[2] = (tm >> 40) & 0xff; | |
| b[3] = (tm >> 32) & 0xff; | |
| b[4] = (tm >> 24) & 0xff; | |
| b[5] = (tm >> 16) & 0xff; | |
| b[6] = (tm >> 8) & 0xff; | |
| b[7] = (tm >> 0) & 0xff; | |
| uint8_t hm[20]; | |
| hmac_sha1(secret, slen, (const uint8_t *) &b, 8, hm); | |
| int offset = hm[19] & 0x0f; | |
| uint32_t code = 0; | |
| code |= hm[offset + 0] << 24; | |
| code |= hm[offset + 1] << 16; | |
| code |= hm[offset + 2] << 8; | |
| code |= hm[offset + 3] << 0; | |
| code &= 0x7fffffff; | |
| code %= 1000000; | |
| memset(secret, 0, t); | |
| munlock(secret, t); | |
| free(secret); | |
| return code; | |
| } | |
| void wipe_stack() { | |
| // dirty little hack | |
| void *stack = alloca(4096); | |
| memset(stack, 0, 4096); | |
| } | |
| int main() { | |
| char *env = getenv("HOME"); | |
| if (env == NULL) { | |
| fprintf(stderr, "HOME not set!\n"); | |
| return 1; | |
| } | |
| if (chdir(env) != 0) { | |
| fprintf(stderr, "Cannot chdir to HOME: %s!\n", strerror(errno)); | |
| return 1; | |
| } | |
| if (chdir(".config") != 0) { | |
| fprintf(stderr, "Cannot chdir to .config: %s!\n", strerror(errno)); | |
| return 1; | |
| } | |
| FILE *fd = fopen("totp", "r"); | |
| if (fd == NULL) { | |
| fprintf(stderr, "Cannot open totp secret file: %s!\n", strerror(errno)); | |
| return 1; | |
| } | |
| char *key = (char *) malloc(257); | |
| mlock(key, 257); | |
| memset(key, 0, 257); | |
| fread(key, 1, 256, fd); | |
| fclose(fd); | |
| for (int i = 256; key[i] != 0x00 && key[i] != 0x0a; i--) | |
| key[i] = 0x00; | |
| printf("%06u\n", totp(key)); | |
| memset(key, 0, 256); | |
| munlock(key, 257); | |
| free(key); | |
| wipe_stack(); | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
licensed under CC0/Public Domain