shithub: totp

ref: d95c83d30d3a9a4d5cba5cc9f803865931333bee
dir: totp/factotum_totp.diff

View raw version
diff f40225e86cf4b92cab975d9121ff06ed5cfe9d91 uncommitted
--- a/sys/src/cmd/auth/factotum/dat.h
+++ b/sys/src/cmd/auth/factotum/dat.h
@@ -228,3 +228,4 @@
 extern Proto httpdigest;		/* httpdigest.c */
 extern Proto ecdsa;			/* ecdsa.c */
 extern Proto wpapsk;			/* wpapsk.c */
+extern Proto totp;			/* totp.c */
--- a/sys/src/cmd/auth/factotum/fs.c
+++ b/sys/src/cmd/auth/factotum/fs.c
@@ -43,6 +43,7 @@
 	&vnc,
 	&ecdsa,
 	&wpapsk,
+	&totp,
 	nil,
 };
 
--- a/sys/src/cmd/auth/factotum/mkfile
+++ b/sys/src/cmd/auth/factotum/mkfile
@@ -14,6 +14,7 @@
 	rsa.$O\
 	ecdsa.$O\
 	wpapsk.$O\
+	totp.$O\
 
 FOFILES=\
 	$PROTO\
--- /dev/null
+++ b/sys/src/cmd/auth/factotum/test.c
@@ -1,0 +1,174 @@
+#include <u.h>
+#include <libc.h>
+#include <auth.h>
+
+void
+main(void)
+{
+	int fd;
+	AuthRpc *rpc;
+	int n;
+	uint r;
+	char *s;
+	char response[8192];
+	char *toks[2];
+	char *otp;
+	char *invalid = "000000";
+	
+	/******/
+	/* generate OTP */
+	
+	fd = open("/mnt/factotum/rpc", ORDWR);
+	if (fd < 0)
+		sysfatal("err: %r");
+	
+	rpc = auth_allocrpc(fd);
+	if (!rpc)
+		sysfatal("err: %r");
+	
+	s = smprint("proto=totp user=a role=client");
+	n = strlen(s);
+	
+	if (auth_rpc(rpc, "start", s, n) != ARok)
+		sysfatal("err: %r");
+	
+	r = auth_rpc(rpc, "read", nil, 0);
+	print("response (%d): %s\n", r, rpc->arg);
+	
+	if (tokenize(rpc->arg, toks, 2) != 2)
+		sysfatal("err: bad number of args in response!");
+	
+	otp = smprint("%s", toks[0]);
+	
+	auth_freerpc(rpc);
+	close(fd);
+	print("client success!\n\n");
+	free(s);
+	
+	/********/
+	/* valid OTP test */
+	
+	fd = open("/mnt/factotum/rpc", ORDWR);
+	if (fd < 0)
+		sysfatal("err: %r");
+	
+	rpc = auth_allocrpc(fd);
+	if (!rpc)
+		sysfatal("err: %r");
+	
+	s = smprint("proto=totp user=a digits=6 role=server");
+	n = strlen(s);
+	
+	if (auth_rpc(rpc, "start", s, n) != ARok)
+		sysfatal("err: %r");
+	
+	print("testing %s\n", otp);
+	r = auth_rpc(rpc, "write", otp, strlen(otp));
+	if (r != ARok)
+		sysfatal("err: %r");
+	
+	r = auth_rpc(rpc, "read", nil, 0);
+	if (r != ARok)
+		print("valid otp: failed: %s\n\n", rpc->arg);
+	else
+		print("valid otp: success: %s\n\n", rpc->arg);
+	
+	auth_freerpc(rpc);
+	close(fd);
+	
+	/*******/
+	/* invalid OTP test */
+	
+	fd = open("/mnt/factotum/rpc", ORDWR);
+	if (fd < 0)
+		sysfatal("err: %r");
+	
+	rpc = auth_allocrpc(fd);
+	if (!rpc)
+		sysfatal("err: %r");
+	
+	if (auth_rpc(rpc, "start", s, n) != ARok)
+		sysfatal("err: %r");
+	
+	print("testing %s\n", invalid);
+	r = auth_rpc(rpc, "write", invalid, strlen(invalid));
+	if (r != ARok)
+		sysfatal("err: %r");
+	
+	r = auth_rpc(rpc, "read", nil, 0);
+	if (r != ARok)
+		print("invalid otp: success: %s\n\n", rpc->arg);
+	else
+		print("invalid otp: failed: %s\n\n", rpc->arg);
+	
+	auth_freerpc(rpc);
+	close(fd);
+	
+	/******/
+	/* generate OTP with digits */
+	
+	fd = open("/mnt/factotum/rpc", ORDWR);
+	if (fd < 0)
+		sysfatal("err: %r");
+	
+	rpc = auth_allocrpc(fd);
+	if (!rpc)
+		sysfatal("err: %r");
+	
+	s = smprint("proto=totp user=b role=client");
+	n = strlen(s);
+	
+	if (auth_rpc(rpc, "start", s, n) != ARok)
+		sysfatal("err: %r");
+	
+	free(s);
+	s = smprint("9 10");
+	n = strlen(s);
+	
+	if (auth_rpc(rpc, "write", s, n) != ARok)
+		sysfatal("err: %r");
+	
+	r = auth_rpc(rpc, "read", nil, 0);
+	print("response (%d): %s\n", r, rpc->arg);
+	
+	if (tokenize(rpc->arg, toks, 2) != 2)
+		sysfatal("err: bad number of args in response!");
+	
+	otp = smprint("%s", toks[0]);
+	
+	auth_freerpc(rpc);
+	close(fd);
+	print("client digits success!\n\n");
+	free(s);
+	
+	/********/
+	/* valid OTP test with digits */
+	
+	fd = open("/mnt/factotum/rpc", ORDWR);
+	if (fd < 0)
+		sysfatal("err: %r");
+	
+	rpc = auth_allocrpc(fd);
+	if (!rpc)
+		sysfatal("err: %r");
+	
+	s = smprint("proto=totp user=b digits=9 role=server");
+	n = strlen(s);
+	
+	if (auth_rpc(rpc, "start", s, n) != ARok)
+		sysfatal("err: %r");
+	
+	print("testing %s\n", otp);
+	r = auth_rpc(rpc, "write", otp, strlen(otp));
+	if (r != ARok)
+		sysfatal("err: %r");
+	
+	r = auth_rpc(rpc, "read", nil, 0);
+	if (r != ARok)
+		print("valid otp digits: failed: %s\n\n", rpc->arg);
+	else
+		print("valid otp digits: success: %s\n\n", rpc->arg);
+	
+	auth_freerpc(rpc);
+	close(fd);
+}
--- /dev/null
+++ b/sys/src/cmd/auth/factotum/test.rc
@@ -1,0 +1,10 @@
+#!/bin/rc
+
+echo 'key proto=totp user=a role=client !secret=abc' >/mnt/factotum/ctl
+echo 'key proto=totp user=a role=server digits=6 !secret=abc' >/mnt/factotum/ctl
+echo 'key proto=totp user=b role=client !secret=def' >/mnt/factotum/ctl
+echo 'key proto=totp user=b role=server digits=9 seconds=10 !secret=def' >/mnt/factotum/ctl
+
+6c -o test.6 test.c
+6l -o 6.test test.6
+6.test
--- /dev/null
+++ b/sys/src/cmd/auth/factotum/totp.c
@@ -1,0 +1,268 @@
+/*
+ * TOTP
+ *
+ * Client protocol:
+ *  write (optional): digits + seconds
+ *  read totp: otp[digits] + time_remaining
+ *
+ * Server protocol:
+ *  write totp: otp[digits]
+ *  read response: done | error
+ *
+ */
+
+#include "dat.h"
+
+uint dohotp(char *key, uvlong counter);
+uint dototp(char *key, long time, int valid);
+
+char *validstr = "valid";
+int validstrlen = -1;
+
+typedef struct State State;
+struct State
+{
+	Key *key;
+	int valid;
+	int seconds;
+	int digits;
+};
+
+enum
+{
+	HaveTotp,
+	HaveDetails,
+	ValidOtp,
+	InvalidOtp,
+	Maxphase,
+};
+
+static char *phasenames[Maxphase] =
+{
+[HaveTotp]    "HaveTotp",
+[HaveDetails] "HaveDetails",
+[ValidOtp]    "ValidOtp",
+[InvalidOtp]  "InvalidOtp",
+};
+
+static int
+totpinit(Proto *p, Fsstate *fss)
+{
+	int ret;
+	Key *k;
+	Keyinfo ki;
+	State *s;
+	
+	ret = findkey(&k, mkkeyinfo(&ki, fss, nil), "%s", p->keyprompt);
+	if (ret != RpcOk)
+		return ret;
+	
+	setattrs(fss->attr, k->attr);
+	s = emalloc(sizeof(*s));
+	s->key = k;
+	fss->ps = s;
+	fss->phase = HaveTotp;
+	return RpcOk;
+}
+
+static void
+totpclose(Fsstate *fss)
+{
+	State *s;
+	
+	s = fss->ps;
+	if (s->key)
+		closekey(s->key);
+	free(s);
+}
+
+static int
+totpread(Fsstate *fss, void *va, uint *n)
+{
+	State *s;
+	char *secret;
+	char decoded[1024];
+	int iscli;
+	uint otp;
+	char *c;
+	int m;
+	long t;
+	
+	if (validstrlen < 0)
+		validstrlen = strlen(validstr);
+	
+	s = fss->ps;
+	switch (fss->phase) {
+	default:
+		return phaseerror(fss, "read");
+		
+	case HaveTotp:
+		iscli = isclient(_strfindattr(s->key->attr, "role"));
+		if (!iscli)
+			return phaseerror(fss, "server protocol must start with a write");
+		s->digits = 6;
+		s->seconds = 30;
+	
+	case HaveDetails:
+		iscli = isclient(_strfindattr(s->key->attr, "role"));
+		if (!iscli)
+			return phaseerror(fss, "you found a bug");
+		
+		secret = _strfindattr(s->key->privattr, "!secret");
+		dec32((uchar*)decoded, 1024, secret, strlen(secret));
+		t = time(nil);
+		otp = dototp(decoded, t, s->seconds);
+		
+		c = smprint("%0*d %ld", s->digits, otp, s->seconds - t%s->seconds);
+		
+		m = strlen(c);
+		if (m > *n)
+			return toosmall(fss, m);
+		
+		*n = m;
+		memmove(va, c, m);
+		free(c);
+		return RpcOk;
+	
+	case ValidOtp:
+		memmove(va, validstr, validstrlen);
+		return RpcOk;
+	
+	case InvalidOtp:
+		return failure(fss, "wrong OTP");
+	}
+}
+
+static int
+checkvalid(Fsstate *fss, State *s, char *c, long t, char *decoded, int seconds, int digits, char *entered)
+{
+	uint otp;
+	
+	otp = dototp(decoded, t, seconds);
+	snprint(c, digits + 1, "%0*d", digits, otp);
+	if (strcmp(c, entered) == 0) {
+		free(c);
+		fss->phase = ValidOtp;
+		s->valid = 1;
+		return 1;
+	}
+	return 0;
+}
+
+static int
+totpwrite(Fsstate *fss, void *va, uint n)
+{
+	char *c;
+	State *s;
+	char *secret;
+	char decoded[1024];
+	char *entered;
+	int iscli;
+	int digits = 6;
+	int seconds = 30;
+	char *toks[2];
+
+	s = fss->ps;
+	switch (fss->phase) {
+	default:
+		return phaseerror(fss, "write");
+	
+	case HaveTotp:
+		iscli = isclient(_strfindattr(s->key->attr, "role"));
+		if (iscli) {
+			c = emalloc(n + 1);
+			memcpy(c, va, n);
+			c[n] = 0;
+			
+			if (tokenize(c, toks, 2) != 2) {
+				free(c);
+				return RpcOk;
+			}
+			s->digits = atoi(toks[0]);
+			if (s->digits < 1)
+				s->digits = 6;
+			s->seconds = atoi(toks[1]);
+			if (s->seconds < 1)
+				s->seconds = 30;
+			free(c);
+			fss->phase = HaveDetails;
+			return RpcOk;
+		}
+		
+		/* server protocol */
+		c = _strfindattr(s->key->attr, "digits");
+		if (c)
+			digits = atoi(c);
+		c = _strfindattr(s->key->attr, "seconds");
+		if (c)
+			seconds = atoi(c);
+		
+		secret = _strfindattr(s->key->privattr, "!secret");
+		dec32((uchar*)decoded, 1024, secret, strlen(secret));
+		
+		entered = emalloc(n + 1);
+		memcpy(entered, va, n);
+		entered[n] = 0;
+		
+		c = malloc(digits + 1);
+		s->valid = 0;
+		
+		if (checkvalid(fss, s, c, time(nil), decoded, seconds, digits, entered))
+			return RpcOk;
+		
+		if (checkvalid(fss, s, c, time(nil) - seconds, decoded, seconds, digits, entered))
+			return RpcOk;
+		
+		if (checkvalid(fss, s, c, time(nil) + seconds, decoded, seconds, digits, entered))
+			return RpcOk;
+		
+		free(c);
+		fss->phase = InvalidOtp;
+		return RpcOk;
+	}
+}
+
+Proto totp =
+{
+.name      = "totp",
+.init      = totpinit,
+.write     = totpwrite,
+.read      = totpread,
+.close     = totpclose,
+.addkey    = replacekey,
+.keyprompt = "user? !secret?",
+};
+
+
+uint
+dohotp(char *key, uvlong counter)
+{
+	uchar hash[SHA1dlen];
+	uchar data[8];
+	data[0] = (counter>>56) & 0xff;
+	data[1] = (counter>>48) & 0xff;
+	data[2] = (counter>>40) & 0xff;
+	data[3] = (counter>>32) & 0xff;
+	data[4] = (counter>>24) & 0xff;
+	data[5] = (counter>>16) & 0xff;
+	data[6] = (counter>>8) & 0xff;
+	data[7] = counter & 0xff;
+	hmac_sha1(data, 8*sizeof(uchar), (uchar*)key, strlen(key), hash, nil);
+	
+	int offset = hash[SHA1dlen - 1] & 0x0F;
+	uint result = (hash[offset] & 0x7F) << 24
+		| (hash[offset + 1] & 0xFF) << 16
+		| (hash[offset + 2] & 0xFF) << 8
+		| hash[offset + 3] & 0xFF;
+	uint _hotp = result % (uint)pow10(6);
+	
+	return _hotp;
+}
+
+uint
+dototp(char *key, long time, int valid)
+{
+	int number = time/(valid <= 0 ? 30 : valid);
+	
+	return dohotp(key, number);
+}