9front - general discussion about 9front
 help / color / mirror / Atom feed
* [9front] Totp in factotum (advice and code)
@ 2023-03-16 19:08 sirjofri
  2023-03-16 19:51 ` [9front] Totp in factotum sirjofri
  2023-03-21 20:10 ` [9front] Re: Totp in factotum (advice and code) sirjofri
  0 siblings, 2 replies; 7+ messages in thread
From: sirjofri @ 2023-03-16 19:08 UTC (permalink / raw)
  To: 9front

Hey all,

as I mentioned before, I am working on totp support in factotum. Here is what I have now:

http://sirjofri.de/oat/patches/totp.zip

(will do it as a proper patch when I'll submit it for inclusion, for future reference.)

I kindly ask for advice about the protocol, which I also describe here shortly for those who don't want to open the zip file:

The client protocol looks like this:

- write (optional): digits + seconds
- read: otp[digits] + time_remaining

This can be used by programs to display the current OTP code in a gui, for example.

The server protocol looks like this:

- write totp: otp[digits]
- read response: "valid" | error

This can be used to verify an entered OTP code.

The keys can look like this:

key proto=totp user=a role=client !secret=abc
key proto=totp user=a role=server digits=6 seconds=30 !secret=abc

Inside the code there are surely potential bugs, leaks, nonsense etc, just so you are prepared.

Note that I plan to submit this to the 9front distribution (if that's welcome), so any advice that can help improve quality is welcome.

sirjofri

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [9front] Totp in factotum
  2023-03-16 19:08 [9front] Totp in factotum (advice and code) sirjofri
@ 2023-03-16 19:51 ` sirjofri
  2023-03-17  9:48   ` cinap_lenrek
  2023-03-21 20:10 ` [9front] Re: Totp in factotum (advice and code) sirjofri
  1 sibling, 1 reply; 7+ messages in thread
From: sirjofri @ 2023-03-16 19:51 UTC (permalink / raw)
  To: 9front

I don't want to clutter this thread, but moody is right, so here's a proper patch (exactly the same content as before).

sirjofri

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);
+}

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [9front] Totp in factotum
  2023-03-16 19:51 ` [9front] Totp in factotum sirjofri
@ 2023-03-17  9:48   ` cinap_lenrek
  2023-03-17 16:43     ` sirjofri
  0 siblings, 1 reply; 7+ messages in thread
From: cinap_lenrek @ 2023-03-17  9:48 UTC (permalink / raw)
  To: 9front

so the key is base32 encoded binary, but the code
uses strlen() on the binary output?

the rfc uses hex encoding, not sure what is
used in practice. the issue with base32 is
that there are many different alphabets
for it. it is not as standartized as
base64.

this looks extreamly fishy and wrong. what if
the binary secret contains nuls? how's the secret
generated? rfc6238 says it should be random
binary.

so i think the functions needs to take a uchar*
for the key and a key-length field, which you
get from dec32() return value.

--
cinap

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [9front] Totp in factotum
  2023-03-17  9:48   ` cinap_lenrek
@ 2023-03-17 16:43     ` sirjofri
  2023-03-18  1:42       ` cinap_lenrek
  0 siblings, 1 reply; 7+ messages in thread
From: sirjofri @ 2023-03-17 16:43 UTC (permalink / raw)
  To: 9front


17.03.2023 10:49:46 cinap_lenrek@felloff.net:

> so the key is base32 encoded binary, but the code
> uses strlen() on the binary output?

You're right, I shouldn't assume the key to be plain text since it can be binary (and probably is). I only tested with "known and easy to type" secrets so far.

> the rfc uses hex encoding, not sure what is
> used in practice. the issue with base32 is
> that there are many different alphabets
> for it. it is not as standartized as
> base64.

Yeah, I'd personally also probably use base64, but in practice the key exchange uses base32 (with the set we have in the standard implementation currently). With key exchange I mean, the encoded url you usually see as a QR code as well as the secret number below that (something like otpauth://). I tested it with microsoft authenticator and it worked in my tests.

I also assume (for the client protocol) that the user gets the code like that, base32 encoded. But I can double check on common sites how they encode it. Maybe it's hex and the base32 is only for the qr code, though I'd doubt that.

> this looks extreamly fishy and wrong. what if
> the binary secret contains nuls? how's the secret
> generated? rfc6238 says it should be random
> binary.

In this case I didn't think about factotum being responsible for generating the secret, only for storing it and using it. Should I assume that factotum also generates secrets itself? That would also only make sense for the server protocol, since the client protocol is for generating OTPs for authenticating the user to external services, like common authenticator apps.

> so i think the functions needs to take a uchar*
> for the key and a key-length field, which you
> get from dec32() return value.

I guess I need a heads up there. When decoding the base32 encoded binary data, how would I know how many bytes I need? Should I just assume that a buffer of length 1024 is enough, or count on dec32 to give me a result and allocate more memory if I need it?

For compatibility, does it make sense to write my own enc32 encoding function, just in case the default changes?

Thanks for your response, I'll further improve it.

sirjofri

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [9front] Totp in factotum
  2023-03-17 16:43     ` sirjofri
@ 2023-03-18  1:42       ` cinap_lenrek
  0 siblings, 0 replies; 7+ messages in thread
From: cinap_lenrek @ 2023-03-18  1:42 UTC (permalink / raw)
  To: 9front

> I guess I need a heads up there. When decoding the base32 encoded binary data,
> how would I know how many bytes I need?
> Should I just assume that a buffer of length 1024 is enough,
> or count on dec32 to give me a result and allocate more memory if I need it?

from the manpage:

         Dec16, dec32 and dec64 return the number of bytes decoded or
          -1 if the decoding fails.  The decoding fails if the output
          buffer is not large enough or, for base 32, if the input
          buffer length is not a multiple of 8.

just using the return value and a fixed 1024 sized buffer is probably fine.
my concern was about the secret containing 0x00 bytes.

otherwise, you can estimate the size by taking the string length of
the input like (strlen(input)*5+8)/8 or something and allocate it.

> For compatibility, does it make sense to write my own enc32 encoding function,
> just in case the default changes?

there is a dec32x() function that you can pass a character converting
function wich maps the alphabet.

see the HISTORY section in the manpage, if they use RFC4648 then
you should be fine. it will break with labs plan9 tho as they still use
the custom plan9 alphabet by default and also have no dec32x() function.

     HISTORY
          In Jan 2018, base 32 encoding was changed from non-standard
          to standard RFC4648 alphabet.

          old: 23456789abcdefghijkmnpqrstuvwxyz

          new: ABCDEFGHIJKLMNOPQRSTUVWXYZ234567

--
cinap

^ permalink raw reply	[flat|nested] 7+ messages in thread

* [9front] Re: Totp in factotum (advice and code)
  2023-03-16 19:08 [9front] Totp in factotum (advice and code) sirjofri
  2023-03-16 19:51 ` [9front] Totp in factotum sirjofri
@ 2023-03-21 20:10 ` sirjofri
  2023-04-05  7:08   ` sirjofri
  1 sibling, 1 reply; 7+ messages in thread
From: sirjofri @ 2023-03-21 20:10 UTC (permalink / raw)
  To: 9front

[-- Attachment #1: Type: text/plain, Size: 188 bytes --]

Hey all,

here's a new version which addresses the mentioned issues:

* Now support for binary secrets
* Many bug fixes
* Proper inclusion of test

This time attached as a file.

sirjofri

[-- Attachment #2.1: Type: text/plain, Size: 340 bytes --]

from postmaster@oat:
The following attachment had content that we can't
prove to be harmless.  To avoid possible automatic
execution, we changed the content headers.
The original header was:

	Content-Type: text/x-diff; charset=us-ascii; name=totp.patch
	Content-Transfer-Encoding: 7bit
	Content-Disposition: attachment; filename=totp.patch

[-- Attachment #2.2: totp.patch.suspect --]
[-- Type: application/octet-stream, Size: 11917 bytes --]

diff 75337cba3a21530c7da1efaa1ab3b6ed145c5172 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/mkfile
@@ -1,0 +1,10 @@
+</$objtype/mkfile
+
+TEST=\
+	totp\
+
+</sys/src/cmd/mktest
+
+totp.test:V: $O.totptest
+	../$O.factotum -n
+	$O.totptest
binary files /tmp/diff100000001531 b/sys/src/cmd/auth/factotum/test/totptest.6 differ
--- /dev/null
+++ b/sys/src/cmd/auth/factotum/test/totptest.c
@@ -1,0 +1,232 @@
+#include <u.h>
+#include <libc.h>
+#include <libsec.h>
+#include <auth.h>
+
+void
+fillfactotum(void)
+{
+	int fd;
+	uchar secret[1024];
+	char encoded[1024];
+	char *s;
+	long len, n = 256;
+	
+	fd = open("/mnt/factotum/ctl", OWRITE);
+	if (fd < 0)
+		sysfatal("err: %r");
+	
+	srand(time(nil));
+	genrandom(secret, n);
+	
+	len = enc32(encoded, 1024, secret, n);
+	if (len < 0)
+		sysfatal("err: %r");
+	
+	print("secret base32 (%ld encoded, %ld original): %s\n", len, n, encoded);
+	
+	// has to use write instead of fprint since fprint uses a 256 buffer
+	
+	s = smprint("key proto=totp user=a role=client !secret=%s\n", encoded);
+	len = write(fd, s, strlen(s));
+	if (len < 0)
+		fprint(2, "err client a: %r\n");
+	free(s);
+	s = smprint("key proto=totp user=a !secret=%s role=server\n", encoded);
+	len = write(fd, s, strlen(s));
+	if (len < 0)
+		fprint(2, "err server a: %r\n");
+	free(s);
+	s = smprint("key proto=totp user=b role=client !secret=%s\n", encoded);
+	len = write(fd, s, strlen(s));
+	if (len < 0)
+		fprint(2, "err client b: %r\n");
+	free(s);
+	s = smprint("key proto=totp user=b role=server digits=9 seconds=10 !secret=%s\n", encoded);
+	len = write(fd, s, strlen(s));
+	if (len < 0)
+		fprint(2, "err server b: %r\n");
+	free(s);
+	
+	close(fd);
+}
+
+void
+main(void)
+{
+	int fd;
+	AuthRpc *rpc;
+	int n;
+	uint r;
+	char *s;
+	char response[8192];
+	char *toks[2];
+	char *otp;
+	char *invalid = "000000";
+	
+	fillfactotum();
+	
+	/******/
+	/* generate OTP */
+	print("\n  Testing client get OTP\n");
+	
+	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("success: client fetch OTP\n\n");
+	free(s);
+	
+	/********/
+	/* valid OTP test */
+	print("\n  Testing server check OTP\n");
+	
+	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=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("failed: valid otp: %s\n\n", rpc->arg);
+	else
+		print("success: valid otp: %s\n\n", rpc->arg);
+	
+	auth_freerpc(rpc);
+	close(fd);
+	
+	/*******/
+	/* invalid OTP test */
+	print("\n  Testing server check invalid OTP\n");
+	
+	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("success: invalid otp: %s\n\n", rpc->arg);
+	else
+		print("failed: invalid otp: %s\n\n", rpc->arg);
+	
+	auth_freerpc(rpc);
+	close(fd);
+	
+	/******/
+	/* generate OTP with digits */
+	print("\n  Testing client get OTP with custom digits/seconds\n");
+	
+	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("success: get client custom\n\n");
+	free(s);
+	
+	/********/
+	/* valid OTP test with digits and seconds (custom) */
+	print("\n  Testing server check OTP with custom digits/seconds\n");
+	
+	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=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("failed: valid otp custom: %s\n\n", rpc->arg);
+	else
+		print("success: valid otp custom: %s\n\n", rpc->arg);
+	
+	auth_freerpc(rpc);
+	close(fd);
+	
+	exits(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/auth/factotum/totp.c
@@ -1,0 +1,296 @@
+/*
+ * 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(uchar *key, ulong keylen, uvlong counter, int digits);
+int dototp(char *key, long time, int valid, int digits, uint *otp);
+
+char *validstr = "valid";
+int validstrlen = -1;
+
+typedef struct State State;
+struct State
+{
+	Key *key;
+	int valid;
+	int seconds;
+	int digits;
+	char *secret;
+};
+
+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;
+	//fss->maxphase = Maxphase;
+	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;
+	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");
+		if (!secret)
+			return failure(fss, "no secret found");
+		
+		t = time(nil);
+		if (dototp(secret, t, s->seconds, s->digits, &otp) < 0)
+			return failure(fss, "can't decode secret");
+		
+		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 *secret, char *entered)
+{
+	uint otp;
+	
+	if (dototp(secret, t, s->seconds, s->digits, &otp) < 0) {
+		s->valid = 0;
+		fss->phase = InvalidOtp;
+		return 0;
+	}
+	
+	snprint(c, s->digits + 1, "%0*d", s->digits, otp);
+	if (strcmp(c, entered) == 0) {
+		free(c);
+		free(entered);
+		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 *entered;
+	ulong t;
+	int iscli;
+	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 */
+		s->digits = 6;
+		s->seconds = 30;
+		c = _strfindattr(s->key->attr, "digits");
+		if (c)
+			s->digits = atoi(c);
+		if (s->digits < 1)
+			s->digits = 1;
+		
+		c = _strfindattr(s->key->attr, "seconds");
+		if (c)
+			s->seconds = atoi(c);
+		if (s->seconds < 1)
+			s->seconds = 1;
+		
+		secret = _strfindattr(s->key->privattr, "!secret");
+		if (!secret)
+			return failure(fss, "no secret found");
+		
+		entered = emalloc(n + 1);
+		memcpy(entered, va, n);
+		entered[n] = 0;
+		
+		c = malloc(s->digits + 1);
+		s->valid = 0;
+		t = time(nil);
+		
+		if (checkvalid(fss, s, c, t, secret, entered))
+			return RpcOk;
+		
+		if (checkvalid(fss, s, c, t - s->seconds, secret, entered))
+			return RpcOk;
+		
+		if (checkvalid(fss, s, c, t + s->seconds, secret, entered))
+			return RpcOk;
+		
+		free(entered);
+		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(uchar *key, ulong keylen, uvlong counter, int digits)
+{
+	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, sizeof(data), key, keylen, 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(digits);
+	
+	return _hotp;
+}
+
+int
+dototp(char *key, long time, int valid, int digits, uint *otp)
+{
+	uchar decoded[1024];
+	long len;
+	int number;
+	
+	len = dec32(decoded, 1024, key, strlen(key));
+	if (len < 0)
+		return -1;
+	
+	number = time/valid;
+	
+	*otp = dohotp(decoded, (ulong)len, number, digits);
+	return 1;
+}


^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [9front] Re: Totp in factotum (advice and code)
  2023-03-21 20:10 ` [9front] Re: Totp in factotum (advice and code) sirjofri
@ 2023-04-05  7:08   ` sirjofri
  0 siblings, 0 replies; 7+ messages in thread
From: sirjofri @ 2023-04-05  7:08 UTC (permalink / raw)
  To: 9front

Bump :)

21.03.2023 21:11:35 sirjofri <sirjofri+ml-9front@sirjofri.de>:
> here's a new version which addresses the mentioned issues:
>
> * Now support for binary secrets
> * Many bug fixes
> * Proper inclusion of test
>
> This time attached as a file.
>
> sirjofri
> from postmaster@oat:
> The following attachment had content that we can't
> prove to be harmless.  To avoid possible automatic
> execution, we changed the content headers.
> The original header was:
>
>     Content-Type: text/x-diff; charset=us-ascii; name=totp.patch
>     Content-Transfer-Encoding: 7bit>     Content-Disposition: attachment; filename=totp.patch


^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2023-04-05  7:09 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-03-16 19:08 [9front] Totp in factotum (advice and code) sirjofri
2023-03-16 19:51 ` [9front] Totp in factotum sirjofri
2023-03-17  9:48   ` cinap_lenrek
2023-03-17 16:43     ` sirjofri
2023-03-18  1:42       ` cinap_lenrek
2023-03-21 20:10 ` [9front] Re: Totp in factotum (advice and code) sirjofri
2023-04-05  7:08   ` sirjofri

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).