Commit Diff


commit - 0ef1e7870cad1d0201d736a31b48187f53a71a9b
commit + ecf69ddfaf6785ad1d8a1dbc59fef74df90291a2
blob - /dev/null
blob + bd76790a065310929666c45bb458a7987ca1cacd (mode 644)
--- /dev/null
+++ totp.1
@@ -0,0 +1,62 @@
+.\" Copyright (c) 2022 Omar Polo <op@omarpolo.com>
+.\"
+.\" Permission to use, copy, modify, and distribute this software for any
+.\" purpose with or without fee is hereby granted, provided that the above
+.\" copyright notice and this permission notice appear in all copies.
+.\"
+.\" 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.
+.Dd October 20, 2022
+.Dt TOTP 1
+.Os
+.Sh NAME
+.Nm totp
+.Nd time-based one time password generator
+.Sh SYNOPSIS
+.Nm
+.No < Ns Ar secret
+.Sh DESCRIPTION
+.Nm
+is a time-based one time password generator
+.Pq TOTP .
+It reads a secret from standard input and prints the generated password,
+a numeric code, to standard output.
+The secret is usually provided by the authenticator
+.Pq for e.g.\& a website .
+Blanks in the secret string are ignored, but only one line is read.
+.Pp
+.Nm
+uses a period of 30 seconds and generates six digits long codes.
+.Sh EXIT STATUS
+.Ex -std
+.Sh EXAMPLES
+.Nm
+is meant to be used with
+.Xr plass 1
+or similar application: the secret is stored safely in the password
+store and then given to
+.Nm
+using a pipe:
+.Bd -literal -offset indent
+$ plass cat 2fa/codeberg/op | totp
+722524
+.Ed
+.Sh SEE ALSO
+.Xr plass 1
+.Sh STANDARDS
+.Nm
+follows the algorithm outlined in RFC 6238
+.Dq TOTP: Time-Based One-Time Password Algorithm
+and uses the base32 encoding as defined in RFC 3548
+.Dq The Base16, Base32, and Base64 Data Encodings .
+.Sh AUTHORS
+.An -nosplit
+The
+.Nm
+utility was written by
+.An Omar Polo Aq Mt op@omarpolo.com .
blob - /dev/null
blob + d2bf3956029a5b1759d0c055abb3920ec1a1c454 (mode 644)
--- /dev/null
+++ totp.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * 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.
+ */
+
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <openssl/evp.h>
+#include <openssl/hmac.h>
+
+#ifndef __OpenBSD__
+# define pledge(a, b) (0)
+#endif
+
+#ifndef __dead
+# define __dead __attribute__((noreturn))
+#endif
+
+#if defined(__FreeBSD__)
+# include <sys/endian.h>
+#elif defined(__APPLE__)
+# include <machine/endian.h>
+# include <libkern/OSByteOrder.h>
+# define htobe64(x) OSSwapHostToBigInt64(x)
+# define be32toh(x) OSSwapBigToHostInt32(x)
+#else
+# include <endian.h>
+#endif
+
+static __dead void
+usage(void)
+{
+	fprintf(stderr, "usage: %s\n", getprogname());
+	exit(1);
+}
+
+static int
+b32c(unsigned char c)
+{
+	if (c >= 'A' && c <= 'Z')
+		return (c - 'A');
+	if (c >= '2' && c <= '7')
+		return (c - '2' + 26);
+	errno = EINVAL;
+	return (-1);
+}
+
+static size_t
+b32decode(char *s, size_t slen, char *q, size_t qlen)
+{
+	int	 i, val[8];
+	char	*t = q;
+
+	while (slen > 0) {
+		memset(val, 0, sizeof(val));
+		for (i = 0; i < 8; ++i) {
+			if (slen == 0)
+				break;
+			if ((val[i] = b32c(*s)) == -1)
+				return (0);
+			s++, slen--;
+		}
+
+		if (qlen < 5) {
+			errno = ENOSPC;
+			return (0);
+		}
+		qlen -= 5;
+
+		*q++ = (val[0] << 3) | (val[1] >> 2);
+		*q++ = ((val[1] & 0x03) << 6) | (val[2] << 1) | (val[3] >> 4);
+		*q++ = ((val[3] & 0x0F) << 4) | (val[4] >> 1);
+		*q++ = ((val[4] & 0x01) << 7) | (val[5] << 2) | (val[6] >> 3);
+		*q++ = ((val[6] & 0x07) << 5) | val[7];
+	}
+
+	return (q - t);
+}
+
+int
+main(int argc, char **argv)
+{
+	char		 buf[1024];
+	size_t		 buflen;
+	unsigned char	 md[EVP_MAX_MD_SIZE];
+	unsigned int	 mdlen;
+	char		*s, *q, *line = NULL;
+	size_t		 linesize = 0;
+	ssize_t		 linelen;
+	uint64_t	 ct;
+	uint32_t	 hash;
+	uint8_t		 off;
+	int		 ch;
+
+	if (pledge("stdio", NULL) == -1)
+		err(1, "pledge");
+
+	while ((ch = getopt(argc, argv, "")) != -1) {
+		switch (ch) {
+		default:
+			usage();
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+	if (argc != 0)
+		usage();
+
+	linelen = getline(&line, &linesize, stdin);
+	if (linelen == -1) {
+		if (ferror(stdin))
+			err(1, "getline");
+		errx(1, "no secret provided");
+	}
+	for (s = q = line; *s != '\0'; ++s) {
+		if (isspace((unsigned char)*s)) {
+			linelen--;
+			continue;
+		}
+		*q++ = *s;
+	}
+	*q = '\0';
+	if (linelen < 1)
+		errx(1, "no secret provided");
+
+	if ((buflen = b32decode(line, linelen, buf, sizeof(buf))) == 0)
+		err(1, "can't base32 decode the secret");
+
+	ct = htobe64(time(NULL) / 30);
+
+	HMAC(EVP_sha1(), buf, buflen, (unsigned char *)&ct, sizeof(ct),
+	    md, &mdlen);
+
+	off = md[mdlen - 1] & 0x0F;
+
+	memcpy(&hash, md + off, sizeof(hash));
+	hash = be32toh(hash);
+	printf("%06d\n", (hash & 0x7FFFFFFF) % 1000000);
+
+	free(line);
+	return (0);
+}