From cb04d87e4b55edbc7dad0b7e27bab7a94af21b6c Mon Sep 17 00:00:00 2001
From: alex <alex@infiniteadaptability.org>
Date: Wed, 15 Jan 2025 08:06:18 -0800
Subject: [PATCH] project creation

initial design complete
---
 .gitignore          | 25 +++++++++++++
 Makefile.am         | 20 ++++++++++
 configure.ac        | 73 ++++++++++++++++++++++++++++++++++++
 inc/args.h          | 17 +++++++++
 inc/base32.h        |  9 +++++
 inc/main.h          | 14 +++++++
 inc/totp.h          | 17 +++++++++
 inc/usage.h         |  8 ++++
 manifest.scm        | 11 ++++++
 src/args.c          | 50 +++++++++++++++++++++++++
 src/base32.c        | 66 ++++++++++++++++++++++++++++++++
 src/main.c          | 34 +++++++++++++++++
 src/totp.c          | 36 ++++++++++++++++++
 src/usage.c         |  8 ++++
 test/Makefile.am    | 23 ++++++++++++
 test/base32.tests.c | 91 +++++++++++++++++++++++++++++++++++++++++++++
 test/base32.tests.h | 10 +++++
 test/test_utils.h   |  8 ++++
 test/totp.tests.c   | 34 +++++++++++++++++
 test/totp.tests.h   | 10 +++++
 20 files changed, 564 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 Makefile.am
 create mode 100644 configure.ac
 create mode 100644 inc/args.h
 create mode 100644 inc/base32.h
 create mode 100644 inc/main.h
 create mode 100644 inc/totp.h
 create mode 100644 inc/usage.h
 create mode 100644 manifest.scm
 create mode 100644 src/args.c
 create mode 100644 src/base32.c
 create mode 100644 src/main.c
 create mode 100644 src/totp.c
 create mode 100644 src/usage.c
 create mode 100644 test/Makefile.am
 create mode 100644 test/base32.tests.c
 create mode 100644 test/base32.tests.h
 create mode 100644 test/test_utils.h
 create mode 100644 test/totp.tests.c
 create mode 100644 test/totp.tests.h

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7cc5ee7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+totp
+*.o
+*.tests
+
+# autoconf/automake
+*.log
+*.trs
+.deps
+.dirstamp
+.lineno
+aclocal.m4
+autom4te.cache/
+autoscan.log
+build-aux/
+config.h
+config.h.in
+config.h.in~
+config.log
+config.status
+configure
+configure~
+configure.scan
+Makefile
+Makefile.in
+stamp-h1
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..8c4aa60
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1,20 @@
+AM_CPPFLAGS = \
+	-Wall \
+	-Werror
+
+bin_PROGRAMS = totp
+totp_SOURCES = \
+	       src/args.c \
+	       src/base32.c \
+	       src/main.c \
+	       src/totp.c \
+	       src/usage.c
+
+totp_SOURCES += \
+		inc/args.h \
+		inc/base32.h \
+		inc/main.h \
+		inc/totp.h \
+		inc/usage.h
+
+SUBDIRS = . test 
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 0000000..d2d12b3
--- /dev/null
+++ b/configure.ac
@@ -0,0 +1,73 @@
+AC_PREREQ([2.69])
+AC_INIT([totp], [1.0.0])
+
+# Store build files not in main directory
+AC_CONFIG_AUX_DIR([build-aux])
+
+AM_INIT_AUTOMAKE([foreign subdir-objects -Wall -Werror])
+
+AC_CONFIG_SRCDIR([src/main.c])
+AC_CONFIG_HEADERS([inc/config.h])
+
+AC_ARG_ENABLE([debug],
+  [AS_HELP_STRING([--enable-debug],
+  [enable debugging])],
+  [enable_debug=$enableval],
+  [enable_debug=no])
+
+AC_ARG_ENABLE([memcheck],
+  [AS_HELP_STRING([--disable-memcheck],
+  [disable valgrind (enabled by default)])],
+  [enable_memcheck=$enableval],
+  [enable_memcheck=yes])
+
+AC_PATH_PROG([VALGRIND], [valgrind])
+AM_CONDITIONAL([HAVE_VALGRIND], [test -n "$VALGRIND"])
+
+AC_MSG_CHECKING([if debugging])
+if test x$enable_debug != xno; then
+	AC_MSG_RESULT(yes)
+	CFLAGS="-ggdb3 -O0"
+else
+	AC_MSG_RESULT(no)
+fi
+
+AM_CONDITIONAL([ENABLE_DEBUG],[test x$enable_debug != xno])
+
+dnl disable memcheck if valgrind not found
+if test "x$enable_memcheck" != "xno"; then
+  if test -z "$VALGRIND"; then
+    enable_memcheck=no
+  fi
+fi
+
+AC_MSG_CHECKING([if memcheck should be enabled])
+if test x$enable_memcheck != xno; then
+  AC_MSG_RESULT(yes)
+else
+  AC_MSG_RESULT(no)
+fi
+
+AM_CONDITIONAL([ENABLE_MEMCHECK],[test x$enable_memcheck = xyes])
+
+# Checks for programs.
+AC_PROG_CC
+AC_PROG_INSTALL
+
+# Checks for libraries.
+AC_CHECK_LIB([crypto], [HMAC])
+
+# Checks for typedefs, structures, and compiler characteristics.
+AC_TYPE_SIZE_T
+AC_TYPE_SSIZE_T
+AC_TYPE_UINT32_T
+
+# Checks for libraries.
+AC_FUNC_MALLOC
+AC_FUNC_MKTIME
+AC_CHECK_FUNCS([floor memset])
+
+AC_CONFIG_FILES([Makefile
+		 test/Makefile])
+
+AC_OUTPUT
diff --git a/inc/args.h b/inc/args.h
new file mode 100644
index 0000000..ebc3cdd
--- /dev/null
+++ b/inc/args.h
@@ -0,0 +1,17 @@
+#ifndef __ARGS_H_
+#define __ARGS_H_
+
+/* needed for strptime */
+#define _XOPEN_SOURCE
+#define _POSIX_C_SOURCE 200809L
+
+#include<getopt.h>
+#include<string.h>
+#include<time.h>
+
+#include<totp.h>
+#include<usage.h>
+
+int args(int, char**);
+
+#endif
diff --git a/inc/base32.h b/inc/base32.h
new file mode 100644
index 0000000..c75ccc0
--- /dev/null
+++ b/inc/base32.h
@@ -0,0 +1,9 @@
+#ifndef __BASE32_H_
+#define __BASE32_H_
+
+#include<stdio.h>
+#include<stdlib.h>
+
+int from_base32(unsigned char**, ssize_t*);
+
+#endif
diff --git a/inc/main.h b/inc/main.h
new file mode 100644
index 0000000..cf632a2
--- /dev/null
+++ b/inc/main.h
@@ -0,0 +1,14 @@
+#ifndef __MAIN_H_
+#define __MAIN_H_
+
+#include<getopt.h>
+#include<stdio.h>
+#include<stdlib.h>
+
+#include<args.h>
+#include<base32.h>
+#include<totp.h>
+
+int main(int,char**);
+
+#endif
diff --git a/inc/totp.h b/inc/totp.h
new file mode 100644
index 0000000..69505be
--- /dev/null
+++ b/inc/totp.h
@@ -0,0 +1,17 @@
+#ifndef __TOTP_H_
+#define __TOTP_H_
+
+#include<math.h>
+#include<stdio.h>
+#include<stdlib.h>
+#include<time.h>
+
+#include<openssl/evp.h>
+#include<openssl/hmac.h>
+#include<openssl/sha.h>
+
+extern time_t now;
+
+int totp(unsigned char*, ssize_t, uint32_t*);
+
+#endif
diff --git a/inc/usage.h b/inc/usage.h
new file mode 100644
index 0000000..c05b501
--- /dev/null
+++ b/inc/usage.h
@@ -0,0 +1,8 @@
+#ifndef __USAGE_H_
+#define __USAGE_H_
+
+#include<stdio.h>
+
+void usage();
+
+#endif
diff --git a/manifest.scm b/manifest.scm
new file mode 100644
index 0000000..6c53f86
--- /dev/null
+++ b/manifest.scm
@@ -0,0 +1,11 @@
+(specifications->manifest (list "autoconf"
+                                "automake"
+                                "coreutils"
+				"gawk"
+                                "gcc-toolchain"
+				"gdb"
+                                "grep"
+                                "make"
+				"openssl"
+                                "sed"
+				"valgrind"))
diff --git a/src/args.c b/src/args.c
new file mode 100644
index 0000000..1ae55f1
--- /dev/null
+++ b/src/args.c
@@ -0,0 +1,50 @@
+#include<args.h>
+
+static struct option long_options[] = {
+	{"help", no_argument, 0, 'h'},
+	{"time", required_argument, 0, 't'},
+	{0,0,0,0}
+};
+
+time_t now = 0;
+
+int args(int argc, char **argv) {
+	struct tm tm;
+	int c;
+
+	while(1) {
+		int option_index = 0;
+
+		if((c = getopt_long(argc, argv, "ht:", long_options, &option_index)) == -1) { break; }
+
+		switch(c) {
+			case 0:
+				if(long_options[option_index].flag != 0) { break; }
+
+				fprintf(stderr, "option %s", long_options[option_index].name);
+				if(optarg) {
+					fprintf(stderr, " with arg %s", optarg);
+				}
+				fprintf(stderr, "\n");
+				return -1;
+				
+				break;
+			case 'h':
+				usage();
+				return -1;
+			case 't':
+				memset(&tm, 0, sizeof(tm));
+
+				if(strptime(optarg, "%F %T %z", &tm) != NULL) {
+					now = mktime(&tm);
+				}
+
+				break;
+			case '?':
+			default:
+				return -1;
+		}
+	}
+
+	return 1;
+}
diff --git a/src/base32.c b/src/base32.c
new file mode 100644
index 0000000..500211e
--- /dev/null
+++ b/src/base32.c
@@ -0,0 +1,66 @@
+#include<base32.h>
+
+int from_base32(unsigned char **raw, ssize_t *n) {
+	unsigned char *buf, *p;
+	char c, to_set;
+	size_t i, j, offset, len;
+
+	if(NULL == raw || NULL == *raw) {
+		fprintf(stderr, "invalid buffer\n");
+		return -1;
+	}
+
+	if(NULL == n || *n <= 0) {
+		fprintf(stderr, "invalid secret size\n");
+		return -1;
+	}
+
+	len = ((*n)*5)>>3;
+	if(len == 0) {
+		fprintf(stderr, "invalid secret size\n");
+		return -1;
+	}
+	
+	buf = calloc(len, sizeof(char));
+	if(NULL == buf) {
+		perror("calloc");
+		return -1;
+	}
+
+	offset = 0;
+	p = *raw;
+	j = 0;
+	for(i = 0; i < *n; i++) {
+		c = p[i];
+
+		if(c >= 'A' && c <= 'Z') {
+			to_set = c - 65;
+		} else if(c >= '2' && c <= '7') {
+			to_set = c - 24;
+		} else if(c == '=') {
+			continue;
+		} else {
+			fprintf(stderr, "invalid base32 character %c\n", c);
+			return -1;
+		}
+
+		// No need to mask this value since it will always be <31
+		buf[j] += (to_set << 3) >> offset;
+
+		if(((offset + 5) > 7) && (j + 1 < len)) {
+			j++;
+			buf[j] += ((to_set << (8 - (5 - (8 - offset)))) & 0xff);
+		}
+
+		offset += 5;
+		offset %= 8;
+	}
+
+	p = *raw;
+	*raw = buf;
+	*n = len;
+
+	free(p);
+
+	return 1;
+}
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..dc228b0
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,34 @@
+#include<main.h>
+
+int main(int argc, char **argv) {
+	unsigned char *buf;
+	size_t len;
+	ssize_t n;
+	uint32_t code;
+
+	buf = NULL;
+	now = 0;
+
+	if(args(argc, argv)<0) { return EXIT_FAILURE; }
+
+	if((n = getline((char **)&buf, &len, stdin)) <= 0) {
+		perror("getline failed");
+		fprintf(stderr, "No secret provided\n");
+		goto clean;
+	}
+
+	// Trim trailing '\n'
+	n--;
+
+	if(from_base32(&buf, &n) < 0) { goto clean; }
+	if(totp((unsigned char *)buf, n, &code) < 0) { goto clean; }
+
+	fprintf(stdout, "%.6u\n", code % 1000000);
+
+	free(buf);
+
+	return EXIT_SUCCESS;
+	clean:
+		if(buf != NULL) { free(buf); }
+		return EXIT_FAILURE;
+}
diff --git a/src/totp.c b/src/totp.c
new file mode 100644
index 0000000..d8c8c6e
--- /dev/null
+++ b/src/totp.c
@@ -0,0 +1,36 @@
+#include<totp.h>
+
+int totp(unsigned char *key, ssize_t n, uint32_t *code) {
+	unsigned char hash[SHA_DIGEST_LENGTH];
+	unsigned char data[sizeof(time_t)];
+	size_t offset;
+	unsigned int hash_len = SHA_DIGEST_LENGTH;
+
+	if(NULL == key) { return -1; }
+	if(n <= 0) { return -1; }
+
+	if(now == 0) {
+		now = time(NULL);
+	}
+
+	now = floor(now / 30);
+
+	// Guarantee that the data buffer is in big-endian order
+	for(size_t i = 0; i<sizeof(time_t); i++) {
+		data[sizeof(time_t) - (1 + i)] = now >> (8*i) & 0xff;
+	}
+
+	if(NULL == HMAC(EVP_sha1(), key, n, data, sizeof(time_t), hash, &hash_len)) {
+		fprintf(stderr, "hashing failed\n");
+		return -1;
+	}
+
+	offset = hash[SHA_DIGEST_LENGTH - 1] & 0xf;
+
+	(*code) = ((hash[offset]  & 0x7f) << 24)
+           | ((hash[offset+1] & 0xff) << 16)
+           | ((hash[offset+2] & 0xff) <<  8)
+           | (hash[offset+3] & 0xff);
+
+	return 1;
+}
diff --git a/src/usage.c b/src/usage.c
new file mode 100644
index 0000000..8500a3e
--- /dev/null
+++ b/src/usage.c
@@ -0,0 +1,8 @@
+#include<usage.h>
+
+void usage() {
+	fprintf(stderr, "Usage:\n");
+	fprintf(stderr, "\t[SECRET] | totp\n");
+	fprintf(stderr, "\n");
+	fprintf(stderr, "Pass a base32 encoded secrets to stdin and generate a totp code at the current time.\n");
+}
diff --git a/test/Makefile.am b/test/Makefile.am
new file mode 100644
index 0000000..795b477
--- /dev/null
+++ b/test/Makefile.am
@@ -0,0 +1,23 @@
+AM_CPPFLAGS = \
+	-Wall \
+	-Werror
+
+EXTRA_DIST = \
+	test_utils.h \
+	base32.tests.h
+
+check_PROGRAMS = base32.tests totp.tests
+TESTS = $(check_PROGRAMS)
+
+if ENABLE_MEMCHECK
+LOG_COMPILER = $(VALGRIND)
+AM_LOG_FLAGS = --leak-check=full -v --track-origins=yes --error-exitcode=1
+endif
+
+base32_tests_SOURCES = \
+	base32.tests.c \
+	$(top_srcdir)/src/base32.c
+
+totp_tests_SOURCES = \
+	totp.tests.c \
+	$(top_srcdir)/src/totp.c
diff --git a/test/base32.tests.c b/test/base32.tests.c
new file mode 100644
index 0000000..4d7fbc3
--- /dev/null
+++ b/test/base32.tests.c
@@ -0,0 +1,91 @@
+#include<base32.tests.h>
+
+int main() {
+	unsigned char *buf;
+	ssize_t n;
+
+	buf = NULL;
+	n = 0;
+
+	assert(from_base32(NULL, NULL) == -1);
+	assert(from_base32(&buf, &n) == -1);
+
+	buf = malloc(3);
+	assert(buf!=NULL);
+	strcpy((char *)buf, "XY");
+
+	assert(from_base32(&buf, &n) == -1);
+
+	n = 2;
+	assert(from_base32(&buf, &n) == 1);
+	assert(buf[0] == 190);
+	assert(n == 1);
+	free(buf);
+
+	buf = malloc(5);
+	assert(buf!=NULL);
+	strcpy((char *)buf, "6LKQ");
+
+	n = 4;
+	assert(from_base32(&buf, &n) == 1);
+	assert(buf[0] == 242);
+	assert(buf[1] == 213);
+	assert(n == 2);
+	free(buf);
+
+	buf = malloc(6);
+	assert(buf!=NULL);
+	strcpy((char *)buf, "LQ7YC");
+
+	n = 5;
+	assert(from_base32(&buf, &n) == 1);
+	assert(buf[0] == 92);
+	assert(buf[1] == 63);
+	assert(buf[2] == 129);
+	assert(n == 3);
+	free(buf);
+
+	buf = malloc(8);
+	assert(buf!=NULL);
+	strcpy((char *)buf, "TOA5RWI");
+
+	n = 7;
+	assert(from_base32(&buf, &n) == 1);
+	assert(buf[0] == 155);
+	assert(buf[1] == 129);
+	assert(buf[2] == 216);
+	assert(buf[3] == 217);
+	assert(n == 4);
+	free(buf);
+
+	buf = malloc(14);
+	assert(buf!=NULL);
+	strcpy((char *)buf, "RZLJOCOTSPYRC");
+
+	n = 13;
+	assert(from_base32(&buf, &n) == 1);
+	assert(buf[0] == 142);
+	assert(buf[1] == 86);
+	assert(buf[2] == 151);
+	assert(buf[3] == 9);
+	assert(buf[4] == 211);
+	assert(buf[5] == 147);
+	assert(buf[6] == 241);
+	assert(buf[7] == 17);
+	assert(n == 8);
+	free(buf);
+
+	buf = malloc(53);
+	assert(buf!=NULL);
+	strcpy((char *)buf, "4KFCI2A3CLB2FV7MAYKSYAXNKFIYH6BOP5UEOOGCF6MLEXTU6KGA");
+
+	n = 52;
+	assert(from_base32(&buf, &n) == 1);
+	unsigned char expected[] = { 226, 138, 36, 104, 27, 18, 195, 162, 215, 236, 6, 21, 44, 2, 237, 81, 81, 131,
+  248, 46, 127, 104, 71, 56, 194, 47, 152, 178, 94, 116, 242, 140 };
+	assert(memcmp(buf, expected, 32) == 0);
+	assert(n == 32);
+	free(buf);
+
+	return EXIT_SUCCESS;
+}
diff --git a/test/base32.tests.h b/test/base32.tests.h
new file mode 100644
index 0000000..9757bab
--- /dev/null
+++ b/test/base32.tests.h
@@ -0,0 +1,10 @@
+#ifndef BASE32_TESTS_H_
+#define BASE32_TESTS_H_
+
+#include<test_utils.h>
+
+#include<base32.h>
+
+int main();
+
+#endif
diff --git a/test/test_utils.h b/test/test_utils.h
new file mode 100644
index 0000000..5b688e9
--- /dev/null
+++ b/test/test_utils.h
@@ -0,0 +1,8 @@
+#ifndef __TEST_UTILS_H_
+#define __TEST_UTILS_H_
+
+#include<assert.h>
+#include<stdlib.h>
+#include<string.h>
+
+#endif
diff --git a/test/totp.tests.c b/test/totp.tests.c
new file mode 100644
index 0000000..36e5346
--- /dev/null
+++ b/test/totp.tests.c
@@ -0,0 +1,34 @@
+#include<totp.tests.h>
+
+time_t now = 0;
+
+int main() {
+	uint32_t code;
+	unsigned char key[] = "12345678901234567890";
+
+	now = 59;
+	assert(totp(key, 20, &code)==1);
+	assert(code%1000000 == 287082);
+
+	now = 1111111109;
+	assert(totp(key, 20, &code)==1);
+	assert(code%1000000 == 81804);
+
+	now = 1111111111;
+	assert(totp(key, 20, &code)==1);
+	assert(code%1000000 == 50471);
+
+	now = 1234567890;
+	assert(totp(key, 20, &code)==1);
+	assert(code%1000000 == 5924);
+
+	now = 2000000000;
+	assert(totp(key, 20, &code)==1);
+	assert(code%1000000 == 279037);
+
+	now = 20000000000;
+	assert(totp(key, 20, &code)==1);
+	assert(code%1000000 == 353130);
+
+	return EXIT_SUCCESS;
+}
diff --git a/test/totp.tests.h b/test/totp.tests.h
new file mode 100644
index 0000000..de74f8d
--- /dev/null
+++ b/test/totp.tests.h
@@ -0,0 +1,10 @@
+#ifndef TOTP_TESTS_H_
+#define TOTP_TESTS_H_
+
+#include<test_utils.h>
+
+#include<totp.h>
+
+int main();
+
+#endif
-- 
2.30.2