ref: d0ab3a0dd8f6355b3603d0fb04043a9ae867639b
dir: /appl/cmd/auth/keyfs.b/
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));
}