shithub: purgatorio

ref: a5cb451b299b03f44154fac5780b6a57ca130ce0
dir: /appl/cmd/auth/keyfs.b/

View raw version
implement Keyfs;

#
# Copyright © 2002,2003 Vita Nuova Holdings Limited.  All rights reserved.
#

include "sys.m";
	sys: Sys;
	Qid: import Sys;

include "draw.m";

include "keyring.m";
	kr: Keyring;
	AESbsize, AESstate: import kr;

include "rand.m";
	rand: Rand;

include "styx.m";
	styx: Styx;
	Tmsg, Rmsg: import styx;

include "styxservers.m";
	styxservers: Styxservers;
	Fid, Styxserver, Navigator, Navop: import styxservers;
	Enotfound, Eperm, Ebadarg, Edot: import styxservers;

include "arg.m";

Keyfs: module
{
	init:	fn(nil: ref Draw->Context, nil: list of string);
};

User: adt
{
	x:	int;		# table index
	name:	string;
	secret:	array of byte;	# eg, password hashed by SHA1
	expire:	int;	# expiration time (epoch seconds)
	status:	int;
	failed:	int;	# count of failed attempts
	path:		big;
};

Qroot, Quser, Qsecret, Qlog, Qstatus, Qexpire: con iota;
files := array[] of {
	(Qsecret, "secret"),
	(Qlog, "log"),
	(Qstatus, "status"),
	(Qexpire, "expire")
};

Maxsecret: con 255;
Maxname: con 255;
Maxfail: con 50;
users: array of ref User;
Sok, Sdisabled: con iota;
status := array[] of {Sok => "ok", Sdisabled => "disabled" };
Never: con 0;	# expiry time

Eremoved: con "user has been removed";

pathgen := 0;
keyversion := 0;
user: string;
now: int;

usage()
{
	sys->fprint(sys->fildes(2), "Usage: keyfs [-D] [-m mountpoint] [keyfile]\n");
	raise "fail:usage";
}

nomod(s: string)
{
	sys->fprint(sys->fildes(2), "keyfs: can't load %s: %r\n", s);
	raise "fail:load";
}

init(nil: ref Draw->Context, args: list of string)
{
	sys = load Sys Sys->PATH;
	sys->pctl(Sys->NEWPGRP, nil);
	kr = load Keyring Keyring->PATH;
	if(kr == nil)
		nomod(Keyring->PATH);
	styx = load Styx Styx->PATH;
	if(styx == nil)
		nomod(Styx->PATH);
	styxservers = load Styxservers Styxservers->PATH;
	if(styxservers == nil)
		nomod(Styxservers->PATH);
	rand = load Rand Rand->PATH;
	if(rand == nil)
		nomod(Rand->PATH);

	styx->init();
	styxservers->init(styx);
	rand->init(sys->millisec());

	arg := load Arg Arg->PATH;
	if(arg == nil)
		nomod(Arg->PATH);
	arg->init(args);
	arg->setusage("keyfs [-m mntpt] [-D] [-n nvramfile] [keyfile]");
	mountpt := "/mnt/keys";
	keyfile := "/keydb/keys";
	nvram: string;
	while((o := arg->opt()) != 0)
		case o {
		'm' =>
			mountpt = arg->earg();
		'D' =>
			styxservers->traceset(1);
		'n' =>
			nvram = arg->earg();
		* =>
			usage();
		}
	args = arg->argv();
	arg = nil;

	if(args != nil)
		keyfile = hd args;

	pwd, err: string;
	if(nvram != nil){
		pwd = rf(nvram);
		if(pwd == nil)
			error(sys->sprint("can't read %s: %r", nvram));
	}
	if(pwd == nil){
		(pwd, err) = readconsline("Key: ", 1);
		if(pwd == nil || err == "exit")
			exit;
		if(err != nil)
			error(sys->sprint("couldn't get key: %s", err));
		(rc, d) := sys->stat(keyfile);
		if(rc == -1 || d.length == big 0){
			pwd0 := pwd;
			(pwd, err) = readconsline("Confirm key: ", 1);
			if(pwd == nil || err == "exit")
				exit;
			if(pwd != pwd0)
				error("key mismatch");
			for(i := 0; i < len pwd0; i++)
				pwd0[i] = ' ';	# clear it out
		}
	}

	thekey = hashkey(pwd);
	for(i:=0; i<len pwd; i++)
		pwd[i] = ' ';	# clear it out

	sys->pctl(Sys->NEWPGRP|Sys->FORKFD, nil);	# immediately avoid sharing keyfd

	readkeys(keyfile);

	user = rf("/dev/user");
	if(user == nil)
		user = "keyfs";

	fds := array[2] of ref Sys->FD;
	if(sys->pipe(fds) < 0)
		error(sys->sprint("can't create pipe: %r"));

	navops := chan of ref Navop;
	spawn navigator(navops);

	(tchan, srv) := Styxserver.new(fds[0], Navigator.new(navops), big Qroot);
	fds[0] = nil;

	pidc := chan of int;
	spawn serveloop(tchan, srv, pidc, navops, keyfile);
	<-pidc;

	if(sys->mount(fds[1], nil, mountpt, Sys->MREPL|Sys->MCREATE, nil) < 0)
		error(sys->sprint("mount on %s failed: %r", mountpt));
}

rf(f: string): string
{
	fd := sys->open(f, Sys->OREAD);
	if(fd == nil)
		return nil;
	b := array[256] of byte;
	n := sys->read(fd, b, len b);
	if(n < 0)
		return nil;
	return string b[0:n];
}

quit(err: string)
{
	fd := sys->open("/prog/"+string sys->pctl(0, nil)+"/ctl", Sys->OWRITE);
	if(fd != nil)
		sys->fprint(fd, "killgrp");
	if(err != nil)
		raise "fail:"+err;
	exit;
}

error(s: string)
{
	sys->fprint(sys->fildes(2), "keyfs: %s\n", s);
	quit("error");
}

thekey: array of byte;

hashkey(s: string): array of byte
{
	key := array of byte s;
	skey := array[Keyring->SHA1dlen] of byte;
	sha := kr->sha1(array of byte "aescbc file", 11, nil, nil);
	kr->sha1(key, len key, skey, sha);
	for(i:=0; i<len key; i++)
		key[i] = byte 0;	# clear it out
#{sys->print("HEX="); for(i:=0;i<len skey&&i<AESbsize; i++)sys->print("%.2ux", int skey[i]);sys->print("\n");}
	return skey[0:AESbsize];
}

readconsline(prompt: string, raw: int): (string, string)
{
	fd := sys->open("/dev/cons", Sys->ORDWR);
	if(fd == nil)
		return (nil, sys->sprint("can't open cons: %r"));
	sys->fprint(fd, "%s", prompt);
	fdctl: ref Sys->FD;
	if(raw){
		fdctl = sys->open("/dev/consctl", sys->OWRITE);
		if(fdctl == nil || sys->fprint(fdctl, "rawon") < 0)
			return (nil, sys->sprint("can't open consctl: %r"));
	}
	line := array[256] of byte;
	o := 0;
	err: string;
	buf := array[1] of byte;
  Read:
	while((r := sys->read(fd, buf, len buf)) > 0){
		c := int buf[0];
		case c {
		16r7F =>
			err = "interrupt";
			break Read;
		'\b' =>
			if(o > 0)
				o--;
		'\n' or '\r' or 16r4 =>
			break Read;
		* =>
			if(o > len line){
				err = "line too long";
				break Read;
			}
			line[o++] = byte c;
		}
	}
	sys->fprint(fd, "\n");
	if(r < 0)
		err = sys->sprint("can't read cons: %r");
	if(raw)
		sys->fprint(fdctl, "rawoff");
	if(err != nil)
		return (nil, err);
	return (string line[0:o], err);
}

serveloop(tchan: chan of ref Tmsg, srv: ref Styxserver, pidc: chan of int, navops: chan of ref Navop, keyfile: string)
{
	pidc <-= sys->pctl(Sys->FORKNS|Sys->NEWFD, 1::2::srv.fd.fd::nil);
	while((gm := <-tchan) != nil){
		now = time();
		pick m := gm {
		Readerror =>
			error(sys->sprint("mount read error: %s", m.error));
		Create =>
			(c, mode, nil, err) := srv.cancreate(m);
			if(c == nil){
				srv.reply(ref Rmsg.Error(m.tag, err));
				break;
			}
			case TYPE(c.path) {	# parent
			Qroot =>
				if((m.perm & Sys->DMDIR) == 0){
					srv.reply(ref Rmsg.Error(m.tag, Eperm));
					break;
				}
				u := findusername(m.name);
				if(u != nil){
					srv.reply(ref Rmsg.Error(m.tag, "user already exists"));
					continue;
				}
				if(len m.name > Maxname){
					srv.reply(ref Rmsg.Error(m.tag, "user name too long"));
					continue;
				}
				u = newuser(m.name, nil);
				qid := Qid((u.path | big Quser), 0, Sys->QTDIR);
				c.open(mode, qid);
				writekeys(keyfile);
				srv.reply(ref Rmsg.Create(m.tag, qid, srv.iounit()));
			* =>
				srv.reply(ref Rmsg.Error(m.tag, Eperm));
				break;
			}
		Read =>
			(c, err) := srv.canread(m);
			if(c == nil){
				srv.reply(ref Rmsg.Error(m.tag, err));
				break;
			}
			if(c.qtype & Sys->QTDIR){
				srv.read(m);	# does readdir
				break;
			}
			u := finduserpath(c.path);
			if(u == nil){
				srv.reply(ref Rmsg.Error(m.tag, Eremoved));
				break;
			}
			case TYPE(c.path) {
			Qsecret =>
				if(u.status != Sok){
					srv.reply(ref Rmsg.Error(m.tag, "user disabled"));
					break;
				}
				if(u.expire < now && u.expire != Never){
					srv.reply(ref Rmsg.Error(m.tag, "user expired"));
					break;
				}
				srv.reply(styxservers->readbytes(m, u.secret));
			Qlog =>
				srv.reply(styxservers->readstr(m, sys->sprint("%d", u.failed)));
			Qstatus =>
				s := status[u.status];
				if(u.status == Sok && u.expire != Never && u.expire < now)
					s = "expired";
				srv.reply(styxservers->readstr(m, s));
			Qexpire =>
				s: string;
				if(u.expire != Never)
					s = sys->sprint("%ud", u.expire);
				else
					s = "never";
				srv.reply(styxservers->readstr(m, s));
			* =>
				srv.reply(ref Rmsg.Error(m.tag, Eperm));
			}
		Write =>
			(c, merr) := srv.canwrite(m);
			if(c == nil){
				srv.reply(ref Rmsg.Error(m.tag, merr));
				break;
			}
			u := finduserpath(c.path);
			if(u == nil){
				srv.reply(ref Rmsg.Error(m.tag, Eremoved));
				break;
			}
		    Case:
			case TYPE(c.path) {
			Qsecret =>
				if(m.offset != big 0 || len m.data > Maxsecret){
					srv.reply(ref Rmsg.Error(m.tag, "illegal write"));
					break;
				}
				u.secret = m.data;
				writekeys(keyfile);
				srv.reply(ref Rmsg.Write(m.tag, len m.data));
			Qexpire =>
				s := trim(string m.data);
				if(s != "never"){
					if(!isnumeric(s)){
						srv.reply(ref Rmsg.Error(m.tag, "illegal expiry time"));
						break;
					}
					u.expire = int s;
				}else
					u.expire = Never;
				u.failed = 0;
				writekeys(keyfile);
				srv.reply(ref Rmsg.Write(m.tag, len m.data));
			Qstatus =>
				s := trim(string m.data);
				for(i := 0; i < len status; i++)
					if(s == status[i]){
						u.status = i;
						if(i == Sok)
							u.failed = 0;
						writekeys(keyfile);
						srv.reply(ref Rmsg.Write(m.tag, len m.data));
						break Case;
					}
				srv.reply(ref Rmsg.Error(m.tag, "unknown status"));
			Qlog =>
				s := trim(string m.data);
				if(s != "good" && s != "ok"){
					if(++u.failed >= Maxfail)
						u.status = Sdisabled;
				}else
					u.failed = 0;
				writekeys(keyfile);
				srv.reply(ref Rmsg.Write(m.tag, len m.data));
			* =>
				srv.reply(ref Rmsg.Error(m.tag, Eperm));
			}
		Remove =>
			c := srv.getfid(m.fid);
			if(c == nil){
				srv.remove(m);	# let it diagnose the errors
				break;
			}
			case TYPE(c.path) {
			Quser =>
				srv.delfid(c);
				u := finduserpath(c.path);
				if(u == nil){
					srv.reply(ref Rmsg.Error(m.tag, Eremoved));
					break;
				}
				removeuser(u);
				writekeys(keyfile);
				srv.reply(ref Rmsg.Remove(m.tag));
			Qsecret =>
				srv.delfid(c);
				u := finduserpath(c.path);
				if(u == nil){
					srv.reply(ref Rmsg.Error(m.tag, Eremoved));
					break;
				}
				u.secret = nil;
				writekeys(keyfile);
				srv.reply(ref Rmsg.Remove(m.tag));
			* =>
				srv.remove(m);	# let it reject it
			}
		Wstat =>
			# rename user
			c := srv.getfid(m.fid);
			if(c == nil || TYPE(c.path) != Quser){
				srv.default(gm);	# let it reject it
				break;
			}
			u := finduserpath(c.path);
			if(u == nil){
				srv.reply(ref Rmsg.Error(m.tag, Eremoved));
				break;
			}
			if((new := m.stat.name) == nil){
				srv.default(gm);
				break;
			}
			if(new == "." || new == ".."){
				srv.reply(ref Rmsg.Error(m.tag, Edot));
				break;
			}
			if(findusername(new) != nil){
				srv.reply(ref Rmsg.Error(m.tag, "user already exists"));
				break;
			}
			# unhashuser(u);
			u.name = new;
			# hashuser(u);
			writekeys(keyfile);
			srv.reply(ref Rmsg.Wstat(m.tag));
		* =>
			srv.default(gm);
		}
	}
	navops <-= nil;		# shut down navigator
}

trim(s: string): string
{
	(nf, flds) := sys->tokenize(s, " \t\n");
	if(nf == 0)
		return nil;
	return hd flds;
}

isnumeric(s: string): int
{
	for(i:=0; i<len s; i++)
		if(!(s[i]>='0' && s[i]<='9'))
			return 0;
	return i>0;
}

TYPE(path: big): int
{
	return int path & 16rF;
}

INDEX(path: big): int
{
	return (int path & 16rFFFF) >> 4;
}

finduserpath(path: big): ref User
{
	i := INDEX(path);
	if(i >= len users || (u := users[i]) == nil || u.path != (path & ~big 16rF))
		return nil;
	return u;
}

findusername(name: string): ref User
{
	for(i := 0; i < len users; i++)
		if((u := users[i]) != nil && u.name == name)
			return u;
	return nil;
}

newuser(name: string, u: ref User): ref User
{
	for(i := 0; i < len users; i++)
		if(users[i] == nil)
			break;
	if(i >= len users)
		users = (array[i+16] of ref User)[0:] = users;
	path := big ((pathgen++ << 16) | (i<<4));
	if(u == nil)
		u = ref User(i, name, nil, Never, Sok, 0, path);
	else{
		u.x = i;
		u.path = path;
	}
	users[i] = u;
	return u;
}

removeuser(u: ref User)
{
	if(u != nil)
		users[u.x] = nil;
}

dirslot(n: int): int
{
	for(i := 0; i < len users; i++){
		u := users[i];
		if(u != nil){
			if(n == 0)
				break;
			n--;
		}
	}
	return i;
}

dir(qid: Sys->Qid, name: string, length: big, perm: int): ref Sys->Dir
{
	d := ref sys->zerodir;
	d.qid = qid;
	if(qid.qtype & Sys->QTDIR)
		perm |= Sys->DMDIR;
	d.mode = perm;
	d.name = name;
	d.uid = user;
	d.gid = user;
	d.length = length;
	d.atime = now;
	d.mtime = now;
	return d;
}

dirgen(p: big, name: string, u: ref User): (ref Sys->Dir, string)
{
	case t := TYPE(p) {
	Qroot =>
		return (dir(Qid(big Qroot, keyversion,Sys->QTDIR), "/", big 0, 8r755), nil);
	Quser =>
		if(name == nil){
			if(u == nil){
				u = finduserpath(p);
				if(u == nil)
					return (nil, Enotfound);
			}
			name = u.name;
		}
		return (dir(Qid(p,0,Sys->QTDIR), name, big 0, 8r500), nil);	# note: unwritable
	* =>
		l := 0;
		if(t == Qsecret){
			if(u == nil)
				u = finduserpath(p);
			if(u != nil)
				l = len u.secret;
		}
		return (dir(Qid(p,0,Sys->QTFILE), name, big l, 8r600), nil);
	}
}

navigator(navops: chan of ref Navop)
{
	while((m := <-navops) != nil){
	   Pick:
		pick n := m {
		Stat =>
			n.reply <-= dirgen(n.path, nil, nil);
		Walk =>
			case TYPE(n.path) {
			Qroot =>
				if(n.name == ".."){
					n.reply <-= dirgen(n.path, nil, nil);
					break;
				}
				u := findusername(n.name);
				if(u == nil){
					n.reply <-= (nil, Enotfound);
					break;
				}
				n.reply <-= dirgen(u.path | big Quser, u.name, u);
			Quser =>
				if(n.name == ".."){
					n.reply <-= dirgen(big Qroot, nil, nil);
					break;
				}
				for(j := 0; j < len files; j++){
					(ftype, name) := files[j];
					if(n.name == name){
						n.reply <-= dirgen((n.path & ~big 16rF) | big ftype, name, nil);
						break Pick;
					}
				}
				n.reply <-= (nil, Enotfound);
			* =>
				if(n.name != ".."){
					n.reply <-= (nil, Enotfound);
					break;
				}
				n.reply <-= dirgen((n.path & ~big 16rF) | big Quser, nil, nil);	# parent directory
			}
		Readdir =>
			case TYPE(n.path) {
			Qroot =>
				for(j := dirslot(n.offset); --n.count >= 0 && j < len users; j++)
					if((u := users[j]) != nil)
						n.reply <-= dirgen(u.path | big Quser, u.name, u);
				n.reply <-= (nil, nil);
			Quser =>
				u := finduserpath(n.path);
				if(u == nil){
					n.reply <-= (nil, Eremoved);
					break;
				}
				for(j := n.offset; --n.count >= 0 && j < len files; j++){
					(ftype, name) := files[j];
					n.reply <-= dirgen((n.path & ~big 16rF)|big ftype, name, u);
				}
				n.reply <-= (nil, nil);
			}
		}
	}
}

timefd: ref Sys->FD;

time(): int
{
	if(timefd == nil){
		timefd = sys->open("/dev/time", Sys->OREAD);
		if(timefd == nil)
			return 0;
	}
	buf := array[128] of byte;
	sys->seek(timefd, big 0, 0);
	n := sys->read(timefd, buf, len buf);
	if(n < 0)
		return 0;
	t := (big string buf[0:n]) / big 1000000;
	return int t;
}

Checkpat: con "XXXXXXXXXXXXXXXX";	# it's what Plan 9's aescbc uses
Checklen: con len Checkpat;

Hdrlen: con 1+1+4;

packedsize(u: ref User): int
{
	return Hdrlen+(1+len array of byte u.name)+(1+len u.secret);
}

pack(u: ref User): array of byte
{
	a := array[packedsize(u)] of byte;
	a[0] = byte u.status;
	a[1] = byte u.failed;
	a[2] = byte u.expire;
	a[3] = byte (u.expire>>8);
	a[4] = byte (u.expire>>16);
	a[5] = byte (u.expire>>24);
	bn := array of byte u.name;
	n := len bn;
	if(n > 255)
		error(sys->sprint("overlong user name: %s", u.name));	# shouldn't happen
	a[6] = byte n;
	a[7:] = bn;
	n += 7;
	a[n] = byte len u.secret;
	a[n+1:] = u.secret;
	return a;
}

unpack(a: array of byte): (ref User, int)
{
	if(len a < Hdrlen+2)
		return (nil, 0);
	u := ref User;
	u.status = int a[0];
	u.failed = int a[1];
	u.expire = (int a[5] << 24) | (int a[4] << 16) | (int a[3] << 8) | int a[2];
	n := int a[6];
	j := 7+n;
	if(j > len a)
		return (nil, 0);
	u.name = string a[7:j];
	if(j >= len a)
		return (nil, 0);
	n = int a[j++];
	if(j+n > len a)
		return (nil, 0);
	if(n > 0){
		u.secret = array[n] of byte;
		u.secret[0:] = a[j:j+n];
	}
	return (u, j+n);
}

corrupt(keyfile: string)
{
	error(sys->sprint("%s: incorrect key or corrupt/damaged keyfile", keyfile));
}

readkeys(keyfile: string)
{
	fd := sys->open(keyfile, Sys->OREAD);
	if(fd == nil)
		error(sys->sprint("can't open %s: %r", keyfile));
	(rc, d) := sys->fstat(fd);
	if(rc < 0)
		error(sys->sprint("can't get status of %s: %r", keyfile));
	length := int d.length;
	if(length == 0)
		return;
	if(length < AESbsize+Checklen)
		corrupt(keyfile);
	buf := array[length] of byte;
	if(sys->read(fd, buf, len buf) != len buf)
		error(sys->sprint("can't read %s: %r", keyfile));
	state := kr->aessetup(thekey, buf[0:AESbsize]);
	if(state == nil)
		error("can't initialise AES");
	kr->aescbc(state, buf[AESbsize:], length-AESbsize, Keyring->Decrypt);
	if(string buf[length-Checklen:] != Checkpat)
		corrupt(keyfile);
	length -= Checklen;
	for(i := AESbsize; i < length;){
		(u, n) := unpack(buf[i:]);
		if(u == nil)
			corrupt(keyfile);
		newuser(u.name, u);
		i += n;
	}
}

writekeys(keyfile: string)
{
	length := 0;
	for(i := 0; i < len users; i++)
		if((u := users[i]) != nil)
			length += packedsize(u);
	if(length == 0){
		# leave it empty for clarity
		fd := sys->create(keyfile, Sys->OWRITE, 8r600);
		if(fd == nil)
			error(sys->sprint("can't create %s: %r", keyfile));
		return;
	}
	length += AESbsize+Checklen;
	buf := array[length] of byte;
	for(i=0; i<AESbsize; i++)
		buf[i] = byte rand->rand(256);
	j := AESbsize;
	for(i = 0; i < len users; i++)
		if((u = users[i]) != nil){
			a := pack(u);
			buf[j:] = a;
			j += len a;
		}
	buf[length-Checklen:] = array of byte Checkpat;
	state := kr->aessetup(thekey, buf[0:AESbsize]);
	if(state == nil)
		error("can't initialise AES");
	kr->aescbc(state, buf[AESbsize:], length-AESbsize, Keyring->Encrypt);
	fd := sys->create(keyfile, Sys->OWRITE, 8r600);
	if(fd == nil)
		error(sys->sprint("can't create %s: %r", keyfile));
	if(sys->write(fd, buf, len buf) != len buf)
		error(sys->sprint("error writing to %s: %r", keyfile));
}