tadd secstored; use readcons - plan9port - [fork] Plan 9 from user space
git clone git://src.adamsgaard.dk/plan9port
Log
Files
Refs
README
LICENSE
---
commit 096ff3e14a188992d2dfe59c7fd3f5d6da791331
parent d93fca6a7ab52f518d3e8aca1fc94139313b97ad
Author: rsc 
Date:   Fri, 11 Feb 2005 19:39:51 +0000

add secstored; use readcons

Diffstat:
  M src/cmd/secstore/aescbc.c           |       2 +-
  M src/cmd/secstore/dirls.c            |       2 +-
  M src/cmd/secstore/mkfile             |       9 +++++++--
  A src/cmd/secstore/secacct.c          |      35 +++++++++++++++++++++++++++++++
  A src/cmd/secstore/secchk.c           |      28 ++++++++++++++++++++++++++++
  M src/cmd/secstore/secstore.c         |      17 +++++++++++------
  M src/cmd/secstore/secstore.h         |       5 +++--
  A src/cmd/secstore/secstored.c        |     420 +++++++++++++++++++++++++++++++
  A src/cmd/secstore/secureidcheck.c    |     446 ++++++++++++++++++++++++++++++
  A src/cmd/secstore/secuser.c          |     244 +++++++++++++++++++++++++++++++
  M src/cmd/secstore/util.c             |      10 ----------

11 files changed, 1196 insertions(+), 22 deletions(-)
---
diff --git a/src/cmd/secstore/aescbc.c b/src/cmd/secstore/aescbc.c
t@@ -75,7 +75,7 @@ main(int argc, char **argv)
                 while(buf[n-1] == '\n')
                         buf[--n] = 0;
         }else{
-                pass = getpassm("aescbc key:");
+                pass = readcons("aescbc key", nil, 1);
                 n = strlen(pass);
                 if(n >= BUF)
                         exits("key too long");
diff --git a/src/cmd/secstore/dirls.c b/src/cmd/secstore/dirls.c
t@@ -64,7 +64,7 @@ dirls(char *path)
         if(path==nil || (ndir = ls(path, &dirbuf)) < 0)
                 return nil;
 
-        qsort(dirbuf, ndir, sizeof dirbuf[0], (int (*)(void *, void *))compare);
+        qsort(dirbuf, ndir, sizeof dirbuf[0], (int (*)(const void *, const void *))compare);
         for(nmwid=lenwid=i=0; i nmwid)
                         nmwid = m;
diff --git a/src/cmd/secstore/mkfile b/src/cmd/secstore/mkfile
t@@ -13,10 +13,15 @@ OFILES =\
         util.$O\
 
 
-TARG=aescbc secstore
+TARG=aescbc secstore secstored secuser
 
 <$PLAN9/src/mkmany
 
-$O.aescbc:        aescbc.$O util.$O  $LIB ${SHORTLIB:%=$LIBDIR/lib%.a}
+$O.aescbc:        aescbc.$O util.$O
         $LD -o $target $prereq $LDFLAGS
 
+$O.secstored: secstored.$O dirls.$O secureidcheck.$O $OFILES
+        $LD -o $target $prereq
+
+$O.secuser: secuser.$O $OFILES
+        $LD -o $target $prereq
diff --git a/src/cmd/secstore/secacct.c b/src/cmd/secstore/secacct.c
t@@ -0,0 +1,35 @@
+#include 
+#include 
+#include 
+
+int verbose = 1;
+static char testmess[] = "__secstore\tPAK\nC=%s\nm=0\n";
+
+void
+main(int argc, char **argv)
+{
+        int n, m, fd;
+        uchar buf[500];
+
+        if(argc != 2)
+                exits("usage: secacct userid");
+
+        n = snprint((char*)buf, sizeof buf, testmess, argv[1]);
+        hnputs(buf, 0x8000+n-2);
+
+        fd = dial("tcp!ruble.cs.bell-labs.com!5356", 0, 0, 0);
+        if(fd < 0)
+                exits("cannot dial ruble");
+        if(write(fd, buf, n) != n || readn(fd, buf, 2) != 2)
+                exits("cannot exchange first round");
+        n = ((buf[0]&0x7f)<<8) + buf[1];
+        if(n+1 > sizeof buf)
+                exits("implausibly large count");
+        m = readn(fd, buf, n);
+        close(fd);
+        if(m != n)
+                fprint(2,"short read from secstore\n");
+        buf[m] = 0;
+        print("%s\n", (char*)buf);
+        exits(0);
+}
diff --git a/src/cmd/secstore/secchk.c b/src/cmd/secstore/secchk.c
t@@ -0,0 +1,28 @@
+#include 
+#include 
+#include 
+#include 
+
+extern char* secureidcheck(char *user, char *response);
+Ndb *db;
+
+void
+main(int argc, char **argv)
+{
+        Ndb *db2;
+
+        if(argc!=2){
+                fprint(2,"usage %s pinsecurid\n", argv[0]);
+                exits("usage");
+        }
+        db = ndbopen("/lib/ndb/auth");
+        if(db == 0)
+                syslog(0, "secstore", "no /lib/ndb/auth");
+        db2 = ndbopen(0);
+        if(db2 == 0)
+                syslog(0, "secstore", "no /lib/ndb/local");
+        db = ndbcat(db, db2);
+        print("user=%s\n", getenv("user"));
+        print("%s\n", secureidcheck(getenv("user"), argv[1]));
+        exits(0);
+}
diff --git a/src/cmd/secstore/secstore.c b/src/cmd/secstore/secstore.c
t@@ -16,6 +16,7 @@ typedef struct AuthConn{
 
 int verbose;
 Nvrsafe nvr;
+char *SECSTORE_DIR;
 
 void
 usage(void)
t@@ -311,7 +312,7 @@ chpasswd(AuthConn *c, char *id)
         // changing our password is vulnerable to connection failure
         for(;;){
                 snprint(prompt, sizeof(prompt), "new password for %s: ", id);
-                newpass = getpassm(prompt);
+                newpass = readcons(prompt, nil, 1);
                 if(newpass == nil)
                         goto Out;
                 if(strlen(newpass) >= 7)
t@@ -324,9 +325,9 @@ chpasswd(AuthConn *c, char *id)
         }
         newpasslen = strlen(newpass);
         snprint(prompt, sizeof(prompt), "retype password: ");
-        passck = getpassm(prompt);
+        passck = readcons(prompt, nil, 1);
         if(passck == nil){
-                fprint(2, "getpassmwd failed\n");
+                fprint(2, "readcons failed\n");
                 goto Out;
         }
         if(strcmp(passck, newpass) != 0){
t@@ -419,7 +420,9 @@ login(char *id, char *dest, int pass_stdin, int pass_nvram)
                 }
                 ntry++;
                 if(!pass_stdin && !pass_nvram){
-                        pass = getpassm("secstore password: ");
+                        pass = readcons("secstore password", nil, 1);
+                        if(pass == nil)
+                                pass = estrdup("");
                         if(strlen(pass) >= sizeof c->pass){
                                 fprint(2, "password too long, skipping secstore login\n");
                                 exits("password too long");
t@@ -444,7 +447,7 @@ login(char *id, char *dest, int pass_stdin, int pass_nvram)
                         fprint(2, "Enter an empty password to quit.\n");
         }
         c->passlen = strlen(c->pass);
-        fprint(2, "%s\n", S);
+        fprint(2, "server: %s\n", S);
         free(S);
         if(readstr(c->conn, s) < 0){
                 c->conn->free(c->conn);
t@@ -460,7 +463,9 @@ login(char *id, char *dest, int pass_stdin, int pass_nvram)
                                 exits("missing PIN+SecureID on standard input");
                         free(PINSTA);
                 }else{
-                        pass = getpassm("STA PIN+SecureID: ");
+                        pass = readcons("STA PIN+SecureID", nil, 1);
+                        if(pass == nil)
+                                pass = estrdup("");
                         strncpy(s+3, pass, (sizeof s)-4);
                         memset(pass, 0, strlen(pass));
                         free(pass);
diff --git a/src/cmd/secstore/secstore.h b/src/cmd/secstore/secstore.h
t@@ -17,7 +17,6 @@ typedef struct PW {
 PW *getPW(char *, int);
 int putPW(PW *);
 void freePW(PW *);
-char* getpassm(const char*);
 
 // *client: SConn, client name, passphrase
 // *server: SConn, (partial) 1st msg, PW entry
t@@ -27,4 +26,6 @@ int PAKserver(SConn *, char *, char *, PW **);
 char *PAK_Hi(char *, char *, mpint *, mpint *);
 
 #define LOG "secstore"
-#define SECSTORE_DIR        "/adm/secstore"
+
+extern        char        *SECSTORE_DIR;
+
diff --git a/src/cmd/secstore/secstored.c b/src/cmd/secstore/secstored.c
t@@ -0,0 +1,420 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include "SConn.h"
+#include "secstore.h"
+
+char *SECSTORE_DIR;
+char* secureidcheck(char *, char *);   // from /sys/src/cmd/auth/
+extern char* dirls(char *path);
+
+int verbose;
+Ndb *db;
+
+static void
+usage(void)
+{
+        fprint(2, "usage: secstored [-R] [-S servername] [-s tcp!*!5356] [-v] [-x netmtpt]\n");
+        exits("usage");
+}
+
+static int
+getdir(SConn *conn, char *id)
+{
+        char *ls, *s; 
+        uchar *msg;
+        int n, len;
+
+        s = emalloc(Maxmsg);
+        snprint(s, Maxmsg, "%s/store/%s", SECSTORE_DIR, id);
+
+        if((ls = dirls(s)) == nil)
+                len = 0;
+        else
+                len = strlen(ls);
+
+        /* send file size */
+        snprint(s, Maxmsg, "%d", len);
+        conn->write(conn, (uchar*)s, strlen(s));
+
+        /* send directory listing in Maxmsg chunks */
+        n = Maxmsg;
+        msg = (uchar*)ls;
+        while(len > 0){
+                if(len < Maxmsg)
+                        n = len;
+                conn->write(conn, msg, n);
+                msg += n;
+                len -= n;
+        }
+        free(s);
+        free(ls);
+        return 0;
+}
+
+char *
+validatefile(char *f)
+{
+        char *nl;
+
+        if(f==nil || *f==0)
+                return nil;
+        if(nl = strchr(f, '\n'))
+                *nl = 0;
+        if(strchr(f,'/') != nil || strcmp(f,"..")==0 || strlen(f) >= 300){
+                syslog(0, LOG, "no slashes allowed: %s\n", f);
+                return nil;
+        }
+        return f;
+}
+
+static int
+getfile(SConn *conn, char *id, char *gf)
+{
+        int n, gd, len;
+        ulong mode;
+        char *s;
+        Dir *st;
+
+        if(strcmp(gf,".")==0)
+                return getdir(conn, id);
+
+        /* send file size */
+        s = emalloc(Maxmsg);
+        snprint(s, Maxmsg, "%s/store/%s/%s", SECSTORE_DIR, id, gf);
+        gd = open(s, OREAD);
+        if(gd < 0){
+                syslog(0, LOG, "can't open %s: %r\n", s);
+                free(s);
+                conn->write(conn, (uchar*)"-1", 2);
+                return -1;
+        }
+        st = dirfstat(gd);
+        if(st == nil){
+                syslog(0, LOG, "can't stat %s: %r\n", s);
+                free(s);
+                conn->write(conn, (uchar*)"-1", 2);
+                return -1;
+        }
+        mode = st->mode;
+        len = st->length;
+        free(st);
+        if(mode & DMDIR) {
+                syslog(0, LOG, "%s should be a plain file, not a directory\n", s);
+                free(s);
+                conn->write(conn, (uchar*)"-1", 2);
+                return -1;
+        }
+        if(len < 0 || len > MAXFILESIZE){
+                syslog(0, LOG, "implausible filesize %d for %s\n", len, gf);
+                free(s);
+                conn->write(conn, (uchar*)"-3", 2);
+                return -1;
+        }
+        snprint(s, Maxmsg, "%d", len);
+        conn->write(conn, (uchar*)s, strlen(s));
+
+        /* send file in Maxmsg chunks */
+        while(len > 0){
+                n = read(gd, s, Maxmsg);
+                if(n <= 0){
+                        syslog(0, LOG, "read error on %s: %r\n", gf);
+                        free(s);
+                        return -1;
+                }
+                conn->write(conn, (uchar*)s, n);
+                len -= n;
+        }
+        close(gd);
+        free(s);
+        return 0;
+}
+
+static int
+putfile(SConn *conn, char *id, char *pf)
+{
+        int n, nw, pd;
+        long len;
+        char s[Maxmsg+1];
+
+        /* get file size */
+        n = readstr(conn, s);
+        if(n < 0){
+                syslog(0, LOG, "remote: %s: %r\n", s);
+                return -1;
+        }
+        len = atoi(s);
+        if(len == -1){
+                syslog(0, LOG, "remote file %s does not exist\n", pf);
+                return -1;
+        }else if(len < 0 || len > MAXFILESIZE){
+                syslog(0, LOG, "implausible filesize %ld for %s\n", len, pf);
+                return -1;
+        }
+
+        /* get file in Maxmsg chunks */
+        if(strchr(pf,'/') != nil || strcmp(pf,"..")==0){
+                syslog(0, LOG, "no slashes allowed: %s\n", pf);
+                return -1;
+        }
+        snprint(s, Maxmsg, "%s/store/%s/%s", SECSTORE_DIR, id, pf);
+        pd = create(s, OWRITE, 0660);
+        if(pd < 0){
+                syslog(0, LOG, "can't open %s: %r\n", s);
+                return -1;
+        }
+        while(len > 0){
+                n = conn->read(conn, (uchar*)s, Maxmsg);
+                if(n <= 0){
+                        syslog(0, LOG, "empty file chunk\n");
+                        return -1;
+                }
+                nw = write(pd, s, n);
+                if(nw != n){
+                        syslog(0, LOG, "write error on %s: %r", pf);
+                        return -1;
+                }
+                len -= n;
+        }
+        close(pd);
+        return 0;
+
+}
+
+static int
+removefile(SConn *conn, char *id, char *f)
+{
+        Dir *d;
+        char buf[Maxmsg];
+
+        snprint(buf, Maxmsg, "%s/store/%s/%s", SECSTORE_DIR, id, f);
+
+        if((d = dirstat(buf)) == nil){
+                snprint(buf, sizeof buf, "remove failed: %r");
+                writerr(conn, buf);
+                return -1;
+        }else if(d->mode & DMDIR){
+                snprint(buf, sizeof buf, "can't remove a directory");
+                writerr(conn, buf);
+                free(d);
+                return -1;
+        }
+
+        free(d);
+        if(remove(buf) < 0){
+                snprint(buf, sizeof buf, "remove failed: %r");
+                writerr(conn, buf);
+                return -1;
+        }
+        return 0;
+}
+
+/* given line directory from accept, returns ipaddr!port */
+static char*
+remoteIP(char *ldir)
+{
+        int fd, n;
+        char rp[100], ap[500];
+
+        snprint(rp, sizeof rp, "%s/remote", ldir);
+        fd = open(rp, OREAD);
+        if(fd < 0)
+                return strdup("?!?");
+        n = read(fd, ap, sizeof ap);
+        if(n <= 0 || n == sizeof ap){
+                fprint(2, "error %d reading %s: %r\n", n, rp);
+                return strdup("?!?");
+        }
+        close(fd);
+        ap[n--] = 0;
+        if(ap[n] == '\n')
+                ap[n] = 0;
+        return strdup(ap);
+}
+
+static int
+dologin(int fd, char *S, int forceSTA)
+{
+        int i, n, rv;
+        char *file, *mess;
+        char msg[Maxmsg+1];
+        PW *pw;
+        SConn *conn;
+
+        pw = nil;
+        rv = -1;
+
+        // collect the first message
+        if((conn = newSConn(fd)) == nil)
+                return -1;
+        if(readstr(conn, msg) < 0){
+                fprint(2, "remote: %s: %r\n", msg);
+                writerr(conn, "can't read your first message");
+                goto Out;
+        }
+
+        // authenticate
+        if(PAKserver(conn, S, msg, &pw) < 0){
+                if(pw != nil)
+                        syslog(0, LOG, "secstore denied for %s", pw->id);
+                goto Out;
+        }
+        if((forceSTA || pw->status&STA) != 0){
+                conn->write(conn, (uchar*)"STA", 3);
+                if(readstr(conn, msg) < 10 || strncmp(msg, "STA", 3) != 0){
+                        syslog(0, LOG, "no STA from %s", pw->id);
+                        goto Out;
+                }
+                mess = secureidcheck(pw->id, msg+3);
+                if(mess != nil){
+                        syslog(0, LOG, "secureidcheck denied %s because %s", pw->id, mess);
+                        goto Out;
+                }
+        }
+        conn->write(conn, (uchar*)"OK", 2);
+        syslog(0, LOG, "AUTH %s", pw->id);
+
+        // perform operations as asked
+        while((n = readstr(conn, msg)) > 0){
+                syslog(0, LOG, "[%s] %s", pw->id, msg);
+
+                if(strncmp(msg, "GET ", 4) == 0){
+                        file = validatefile(msg+4);
+                        if(file==nil || getfile(conn, pw->id, file) < 0)
+                                goto Err;
+
+                }else if(strncmp(msg, "PUT ", 4) == 0){
+                        file = validatefile(msg+4);
+                        if(file==nil || putfile(conn, pw->id, file) < 0){
+                                syslog(0, LOG, "failed PUT %s/%s", pw->id, file);
+                                goto Err;
+                        }
+
+                }else if(strncmp(msg, "RM ", 3) == 0){
+                        file = validatefile(msg+3);
+                        if(file==nil || removefile(conn, pw->id, file) < 0){
+                                syslog(0, LOG, "failed RM %s/%s", pw->id, file);
+                                goto Err;
+                        }
+
+                }else if(strncmp(msg, "CHPASS", 6) == 0){
+                        if(readstr(conn, msg) < 0){
+                                syslog(0, LOG, "protocol botch CHPASS for %s", pw->id);
+                                writerr(conn, "protocol botch while setting PAK");
+                                goto Out;
+                        }
+                        pw->Hi = strtomp(msg, nil, 64, pw->Hi);
+                        for(i=0; i < 4 && putPW(pw) < 0; i++)
+                                syslog(0, LOG, "password change failed for %s (%d): %r", pw->id, i);
+                        if(i==4)
+                                goto Out;
+
+                }else if(strncmp(msg, "BYE", 3) == 0){
+                        rv = 0;
+                        break;
+
+                }else{
+                        writerr(conn, "unrecognized operation");
+                        break;
+                }
+
+        }
+        if(n <= 0)
+                syslog(0, LOG, "%s closed connection without saying goodbye\n", pw->id);
+
+Out:
+        freePW(pw);
+        conn->free(conn);
+        return rv;
+Err:
+        writerr(conn, "operation failed");
+        goto Out;
+}
+
+void
+main(int argc, char **argv)
+{
+        int afd, dfd, lcfd, forceSTA = 0;
+        char adir[40], ldir[40], *remote;
+        char *serve = "tcp!*!5356", *p, aserve[128];
+        char *S = "secstore";
+        char *dbpath;
+        Ndb *db2;
+
+        S = sysname();
+        SECSTORE_DIR = unsharp("#9/secstore");
+//        setnetmtpt(net, sizeof(net), nil);
+        ARGBEGIN{
+        case 'R':
+                forceSTA = 1;
+                break;
+        case 's':
+                serve = EARGF(usage());
+                break;
+        case 'S':
+                S = EARGF(usage());
+                break;
+        case 'x':
+                p = ARGF();
+                if(p == nil)
+                        usage();
+                USED(p);
+        //        setnetmtpt(net, sizeof(net), p);
+                break;
+        case 'v':
+                verbose++;
+                break;
+        default:
+                usage();
+        }ARGEND;
+
+        if(!verbose)
+                switch(rfork(RFNOTEG|RFPROC|RFFDG)) {
+                case -1:
+                        sysfatal("fork: %r");
+                case 0:
+                        break;
+                default:
+                        exits(0);
+                }
+
+        snprint(aserve, sizeof aserve, "%s", serve);
+        afd = announce(aserve, adir);
+        if(afd < 0)
+                sysfatal("%s: %r\n", aserve);
+        syslog(0, LOG, "ANNOUNCE %s", aserve);
+        for(;;){
+                if((lcfd = listen(adir, ldir)) < 0)
+                        exits("can't listen");
+                switch(fork()){
+                case -1:
+                        fprint(2, "secstore forking: %r\n");
+                        close(lcfd);
+                        break;
+                case 0:
+                        // "/lib/ndb/common.radius does not exist" if db set before fork
+                        db = ndbopen(dbpath=unsharp("#9/ndb/auth"));
+                        if(db == 0)
+                                syslog(0, LOG, "no ndb/auth");
+                        db2 = ndbopen(0);
+                        if(db2 == 0)
+                                syslog(0, LOG, "no ndb/local");
+                        db = ndbcat(db, db2);
+                        if((dfd = accept(lcfd, ldir)) < 0)
+                                exits("can't accept");
+                        alarm(30*60*1000);         // 30 min
+                        remote = remoteIP(ldir);
+                        syslog(0, LOG, "secstore from %s", remote);
+                        free(remote);
+                        dologin(dfd, S, forceSTA);
+                        exits(nil);
+                default:
+                        close(lcfd);
+                        break;
+                }
+        }
+}
+
diff --git a/src/cmd/secstore/secureidcheck.c b/src/cmd/secstore/secureidcheck.c
t@@ -0,0 +1,446 @@
+/* RFC2138 */
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#define AUTHLOG "auth"
+
+enum{        R_AccessRequest=1,        /* Packet code */
+        R_AccessAccept=2,
+        R_AccessReject=3,
+        R_AccessChallenge=11,
+        R_UserName=1,
+        R_UserPassword=2,
+        R_NASIPAddress=4,
+        R_ReplyMessage=18,
+        R_State=24,
+        R_NASIdentifier=32
+};
+
+typedef struct Secret{
+        uchar *s;
+        int len;
+} Secret;
+
+typedef struct Attribute{
+        struct Attribute *next;
+        uchar type;
+        uchar len;        // number of bytes in value
+        uchar val[256];
+} Attribute;
+
+typedef struct Packet{
+        uchar code, ID;
+        uchar authenticator[16];
+        Attribute first;
+} Packet;
+
+// assumes pass is at most 16 chars
+void
+hide(Secret *shared, uchar *auth, Secret *pass, uchar *x)
+{
+        DigestState *M;
+        int i, n = pass->len;
+
+        M = md5(shared->s, shared->len, nil, nil);
+        md5(auth, 16, x, M);
+        if(n > 16)
+                n = 16;
+        for(i = 0; i < n; i++)
+                x[i] ^= (pass->s)[i];
+}
+
+int
+authcmp(Secret *shared, uchar *buf, int m, uchar *auth)
+{
+        DigestState *M;
+        uchar x[16];
+
+        M = md5(buf, 4, nil, nil); // Code+ID+Length
+        M = md5(auth, 16, nil, M); // RequestAuth
+        M = md5(buf+20, m-20, nil, M); // Attributes
+        md5(shared->s, shared->len, x, M);
+        return memcmp(x, buf+4, 16);
+}
+
+Packet*
+newRequest(uchar *auth)
+{
+        static uchar ID = 0;
+        Packet *p;
+
+        p = (Packet*)malloc(sizeof(*p));
+        if(p == nil)
+                return nil;
+        p->code = R_AccessRequest;
+        p->ID = ++ID;
+        memmove(p->authenticator, auth, 16);
+        p->first.next = nil;
+        p->first.type = 0;
+        return p;
+}
+
+void
+freePacket(Packet *p)
+{
+        Attribute *a, *x;
+
+        if(!p)
+                return;
+        a = p->first.next;
+        while(a){
+                x = a;
+                a = a->next;
+                free(x);
+        }
+        free(p);
+}
+
+int
+ding(void *v, char *msg)
+{
+        USED(v);
+/*        syslog(0, AUTHLOG, "ding %s", msg); */
+        if(strstr(msg, "alarm"))
+                return 1;
+        return 0;
+}
+
+Packet *
+rpc(char *dest, Secret *shared, Packet *req)
+{
+        uchar buf[4096], buf2[4096], *b, *e;
+        Packet *resp;
+        Attribute *a;
+        int m, n, fd, try;
+
+        // marshal request
+        e = buf + sizeof buf;
+        buf[0] = req->code;
+        buf[1] = req->ID;
+        memmove(buf+4, req->authenticator, 16);
+        b = buf+20;
+        for(a = &req->first; a; a = a->next){
+                if(b + 2 + a->len > e)
+                        return nil;
+                *b++ = a->type;
+                *b++ = 2 + a->len;
+                memmove(b, a->val, a->len);
+                b += a->len;
+        }
+        n = b-buf;
+        buf[2] = n>>8;
+        buf[3] = n;
+
+        // send request, wait for reply
+        fd = dial(dest, 0, 0, 0);
+        if(fd < 0){
+                syslog(0, AUTHLOG, "%s: rpc can't get udp channel", dest);
+                return nil;
+        }
+        atnotify(ding, 1);
+        m = -1;
+        for(try = 0; try < 2; try++){
+                alarm(4000);
+                m = write(fd, buf, n);
+                if(m != n){
+                        syslog(0, AUTHLOG, "%s: rpc write err %d %d: %r", dest, m, n);
+                        m = -1;
+                        break;
+                }
+                m = read(fd, buf2, sizeof buf2);
+                alarm(0);
+                if(m < 0){
+                        syslog(0, AUTHLOG, "%s rpc read err %d: %r", dest, m);
+                        break; // failure
+                }
+                if(m == 0 || buf2[1] != buf[1]){  // need matching ID
+                        syslog(0, AUTHLOG, "%s unmatched reply %d", dest, m);
+                        continue;
+                }
+                if(authcmp(shared, buf2, m, buf+4) == 0)
+                        break;
+                syslog(0, AUTHLOG, "%s bad rpc chksum", dest);
+        }
+        close(fd);
+        if(m <= 0)
+                return nil;
+
+        // unmarshal reply
+        b = buf2;
+        e = buf2+m;
+        resp = (Packet*)malloc(sizeof(*resp));
+        if(resp == nil)
+                return nil;
+        resp->code = *b++;
+        resp->ID = *b++;
+        n = *b++;
+        n = (n<<8) | *b++;
+        if(m != n){
+                syslog(0, AUTHLOG, "rpc got %d bytes, length said %d", m, n);
+                if(m > n)
+                        e = buf2+n;
+        }
+        memmove(resp->authenticator, b, 16);
+        b += 16;
+        a = &resp->first;
+        a->type = 0;
+        while(1){
+                if(b >= e){
+                        a->next = nil;
+                        break;                        // exit loop
+                }
+                a->type = *b++;
+                a->len = (*b++) - 2;
+                if(b + a->len > e){ // corrupt packet
+                        a->next = nil;
+                        freePacket(resp);
+                        return nil;
+                }
+                memmove(a->val, b, a->len);
+                b += a->len;
+                if(b < e){  // any more attributes?
+                        a->next = (Attribute*)malloc(sizeof(*a));
+                        if(a->next == nil){
+                                free(req);
+                                return nil;
+                        }
+                        a = a->next;
+                }
+        }
+        return resp;
+}
+
+int
+setAttribute(Packet *p, uchar type, uchar *s, int n)
+{
+        Attribute *a;
+
+        a = &p->first;
+        if(a->type != 0){
+                a = (Attribute*)malloc(sizeof(*a));
+                if(a == nil)
+                        return -1;
+                a->next = p->first.next;
+                p->first.next = a;
+        }
+        a->type = type;
+        a->len = n;
+        if(a->len > 253 )  // RFC2138, section 5
+                a->len = 253;
+        memmove(a->val, s, a->len);
+        return 0;
+}
+
+/* return a reply message attribute string */
+char*
+replymsg(Packet *p)
+{
+        Attribute *a;
+        static char buf[255];
+
+        for(a = &p->first; a; a = a->next){
+                if(a->type == R_ReplyMessage){
+                        if(a->len >= sizeof buf)
+                                a->len = sizeof(buf)-1;
+                        memmove(buf, a->val, a->len);
+                        buf[a->len] = 0;
+                }
+        }
+        return buf;
+}
+
+/* for convenience while debugging */
+char *replymess;
+Attribute *stateattr;
+
+void
+logPacket(Packet *p)
+{
+        Attribute *a;
+        char buf[255];
+        char pbuf[4*1024];
+        uchar *au = p->authenticator;
+        int i;
+        char *np, *e;
+
+        e = pbuf + sizeof(pbuf);
+
+        np = seprint(pbuf, e, "Packet ID=%d auth=%x %x %x... ", p->ID, au[0], au[1], au[2]);
+        switch(p->code){
+        case R_AccessRequest:
+                np = seprint(np, e, "request\n");
+                break;
+        case R_AccessAccept:
+                np = seprint(np, e, "accept\n");
+                break;
+        case R_AccessReject:
+                np = seprint(np, e, "reject\n");
+                break;
+        case R_AccessChallenge:
+                np = seprint(np, e, "challenge\n");
+                break;
+        default:
+                np = seprint(np, e, "code=%d\n", p->code);
+                break;
+        }
+        replymess = "0000000";
+        for(a = &p->first; a; a = a->next){
+                if(a->len > 253 )
+                        a->len = 253;
+                memmove(buf, a->val, a->len);
+                np = seprint(np, e, " [%d]", a->type);
+                for(i = 0; ilen; i++)
+                        if(isprint(a->val[i]))
+                                np = seprint(np, e, "%c", a->val[i]);
+                        else
+                                np = seprint(np, e, "\\%o", a->val[i]);
+                np = seprint(np, e, "\n");
+                buf[a->len] = 0;
+                if(a->type == R_ReplyMessage)
+                        replymess = strdup(buf);
+                else if(a->type == R_State)
+                        stateattr = a;
+        }
+
+        syslog(0, AUTHLOG, "%s", pbuf);
+}
+
+static uchar*
+getipv4addr(void)
+{
+        Ipifc *nifc;
+        Iplifc *lifc;
+        static Ipifc *ifc;
+
+        ifc = readipifc("/net", ifc, -1);
+        for(nifc = ifc; nifc; nifc = nifc->next)
+                for(lifc = nifc->lifc; lifc; lifc = lifc->next)
+                        if(ipcmp(lifc->ip, IPnoaddr) != 0 && ipcmp(lifc->ip, v4prefix) != 0)
+                                return lifc->ip;
+        return nil;
+}
+
+extern Ndb *db;
+
+/* returns 0 on success, error message on failure */
+char*
+secureidcheck(char *user, char *response)
+{
+        Packet *req = nil, *resp = nil;
+        ulong u[4];
+        uchar x[16];
+        char *radiussecret;
+        char ruser[ 64];
+        char dest[3*IPaddrlen+20];
+        Secret shared, pass;
+        char *rv = "authentication failed";
+        Ndbs s;
+        Ndbtuple *t, *nt, *tt;
+        uchar *ip;
+        static Ndb *netdb;
+
+        if(netdb == nil)
+                netdb = ndbopen(0);
+
+        /* bad responses make them disable the fob, avoid silly checks */
+        if(strlen(response) < 4 || strpbrk(response,"abcdefABCDEF") != nil)
+                goto out;
+
+        /* get radius secret */
+        radiussecret = ndbgetvalue(db, &s, "radius", "lra-radius", "secret", &t);
+        if(radiussecret == nil){
+                syslog(0, AUTHLOG, "secureidcheck: nil radius secret: %r");
+                goto out;
+        }
+
+        /* translate user name if we have to */
+        strcpy(ruser, user);
+        for(nt = t; nt; nt = nt->entry){
+                if(strcmp(nt->attr, "uid") == 0 && strcmp(nt->val, user) == 0)
+                        for(tt = nt->line; tt != nt; tt = tt->line)
+                                if(strcmp(tt->attr, "rid") == 0){
+                                        strcpy(ruser, tt->val);
+                                        break;
+                                }
+        }
+        ndbfree(t);
+
+        u[0] = fastrand();
+        u[1] = fastrand();
+        u[2] = fastrand();
+        u[3] = fastrand();
+        req = newRequest((uchar*)u);
+        if(req == nil)
+                goto out;
+        shared.s = (uchar*)radiussecret;
+        shared.len = strlen(radiussecret);
+        ip = getipv4addr();
+        if(ip == nil){
+                syslog(0, AUTHLOG, "no interfaces: %r\n");
+                goto out;
+        }
+        if(setAttribute(req, R_NASIPAddress, ip + IPv4off, 4) < 0)
+                goto out;
+
+        if(setAttribute(req, R_UserName, (uchar*)ruser, strlen(ruser)) < 0)
+                goto out;
+        pass.s = (uchar*)response;
+        pass.len = strlen(response);
+        hide(&shared, req->authenticator, &pass, x);
+        if(setAttribute(req, R_UserPassword, x, 16) < 0)
+                goto out;
+
+        t = ndbsearch(netdb, &s, "sys", "lra-radius");
+        if(t == nil){
+                syslog(0, AUTHLOG, "secureidcheck: nil radius sys search: %r\n");
+                goto out;
+        }
+        for(nt = t; nt; nt = nt->entry){
+                if(strcmp(nt->attr, "ip") != 0)
+                        continue;
+
+                snprint(dest,sizeof dest,"udp!%s!oradius", nt->val);
+                resp = rpc(dest, &shared, req);
+                if(resp == nil){
+                        syslog(0, AUTHLOG, "%s nil response", dest);
+                        continue;
+                }
+                if(resp->ID != req->ID){
+                        syslog(0, AUTHLOG, "%s mismatched ID  req=%d resp=%d",
+                                dest, req->ID, resp->ID);
+                        freePacket(resp);
+                        resp = nil;
+                        continue;
+                }
+        
+                switch(resp->code){
+                case R_AccessAccept:
+                        syslog(0, AUTHLOG, "%s accepted ruser=%s", dest, ruser);
+                        rv = nil;
+                        break;
+                case R_AccessReject:
+                        syslog(0, AUTHLOG, "%s rejected ruser=%s %s", dest, ruser, replymsg(resp));
+                        rv = "secureid failed";
+                        break;
+                case R_AccessChallenge:
+                        syslog(0, AUTHLOG, "%s challenge ruser=%s %s", dest, ruser, replymsg(resp));
+                        rv = "secureid out of sync";
+                        break;
+                default:
+                        syslog(0, AUTHLOG, "%s code=%d ruser=%s %s", dest, resp->code, ruser, replymsg(resp));
+                        break;
+                }
+                break; // we have a proper reply, no need to ask again
+        }
+        ndbfree(t);
+        free(radiussecret);
+out:
+        freePacket(req);
+        freePacket(resp);
+        return rv;
+}
diff --git a/src/cmd/secstore/secuser.c b/src/cmd/secstore/secuser.c
t@@ -0,0 +1,244 @@
+#include 
+#include 
+#include 
+#include 
+#include "SConn.h"
+#include "secstore.h"
+
+int verbose;
+
+static void userinput(char *, int);
+char *SECSTORE_DIR;
+
+static void
+ensure_exists(char *f, ulong perm)
+{
+        int fd;
+
+        if(access(f, AEXIST) >= 0)
+                return;
+        if(verbose)
+                fprint(2,"first time setup for secstore: create %s %lo\n", f, perm);
+        fd = create(f, OREAD, perm);
+        if(fd < 0){
+                fprint(2, "unable to create %s\n", f);
+                exits("secstored directories");
+        }
+        close(fd);
+}
+
+
+int
+main(int argc, char **argv)
+{
+        int isnew;
+        char *id, buf[Maxmsg], home[Maxmsg], prompt[100], *hexHi;
+        char *pass, *passck;
+        long expsecs;
+        mpint *H = mpnew(0), *Hi = mpnew(0);
+        PW *pw;
+        Tm *tm;
+
+        SECSTORE_DIR = unsharp("#9/secstore");
+
+        ARGBEGIN{
+        case 'v':
+                verbose++;
+                break;
+        }ARGEND;
+        if(argc!=1){
+                print("usage: secuser [-v] \n");
+                exits("usage");
+        }
+
+        ensure_exists(SECSTORE_DIR, DMDIR|0755L);
+        snprint(home, sizeof(home), "%s/who", SECSTORE_DIR);
+        ensure_exists(home, DMDIR|0755L);
+        snprint(home, sizeof(home), "%s/store", SECSTORE_DIR);
+        ensure_exists(home, DMDIR|0700L);
+
+        id = argv[0];
+        if(verbose)
+                fprint(2,"secuser %s\n", id);
+        if((pw = getPW(id,1)) == nil){
+                isnew = 1;
+                print("new account (because %s/%s %r)\n", SECSTORE_DIR, id);
+                pw = emalloc(sizeof(*pw));
+                pw->id = estrdup(id);
+                snprint(home, sizeof(home), "%s/store/%s", SECSTORE_DIR, id);
+                if(access(home, AEXIST) == 0){
+                        print("new user, but directory %s already exists\n", home);
+                        exits(home);
+                }
+        }else{
+                isnew = 0;
+        }
+
+        /* get main password for id */
+        for(;;){
+                if(isnew)
+                        snprint(prompt, sizeof(prompt), "%s password", id);
+                else
+                        snprint(prompt, sizeof(prompt), "%s password [default = don't change]", id);
+                pass = readcons(prompt, nil, 1);
+                if(pass == nil){
+                        print("getpass failed\n");
+                        exits("getpass failed");
+                }
+                if(verbose)
+                        print("%ld characters\n", strlen(pass));
+                if(pass[0] == '\0' && isnew == 0)
+                        break;
+                if(strlen(pass) >= 7)
+                        break;
+                print("password must be at least 7 characters\n");
+        }
+
+        if(pass[0] != '\0'){
+                snprint(prompt, sizeof(prompt), "retype password");
+                if(verbose)
+                        print("confirming...\n");
+                passck = readcons(prompt, nil, 1);
+                if(passck == nil){
+                        print("getpass failed\n");
+                        exits("getpass failed");
+                }
+                if(strcmp(pass, passck) != 0){
+                        print("passwords didn't match\n");
+                        exits("no match");
+                }
+                memset(passck, 0, strlen(passck));
+                free(passck);
+                hexHi = PAK_Hi(id, pass, H, Hi);
+                memset(pass, 0, strlen(pass));
+                free(pass);
+                free(hexHi);
+                mpfree(H);
+                pw->Hi = Hi;
+        }
+
+        /* get expiration time (midnight of date specified) */
+        if(isnew)
+                expsecs = time(0) + 365*24*60*60;
+        else
+                expsecs = pw->expire;
+
+        for(;;){
+                tm = localtime(expsecs);
+                print("expires [DDMMYYYY, default = %2.2d%2.2d%4.4d]: ",
+                                tm->mday, tm->mon, tm->year+1900);
+                userinput(buf, sizeof(buf));
+                if(strlen(buf) == 0)
+                        break;
+                if(strlen(buf) != 8){
+                        print("!bad date format: %s\n", buf);
+                        continue;
+                }
+                tm->mday = (buf[0]-'0')*10 + (buf[1]-'0');
+                if(tm->mday > 31 || tm->mday < 1){
+                        print("!bad day of month: %d\n", tm->mday);
+                        continue;
+                }
+                tm->mon = (buf[2]-'0')*10 + (buf[3]-'0') - 1;
+                if(tm->mon > 11 || tm->mday < 0){
+                        print("!bad month: %d\n", tm->mon + 1);
+                        continue;
+                }
+                tm->year = atoi(buf+4) - 1900;
+                if(tm->year < 70){
+                        print("!bad year: %d\n", tm->year + 1900);
+                        continue;
+                }
+                tm->sec = 59;
+                tm->min = 59;
+                tm->hour = 23;
+                tm->yday = 0;
+                expsecs = tm2sec(tm);
+                break;
+        }
+        pw->expire = expsecs;
+
+        /* failed logins */
+        if(pw->failed != 0 )
+                print("clearing %d failed login attempts\n", pw->failed);
+        pw->failed = 0;
+
+        /* status bits */
+        if(isnew)
+                pw->status = Enabled;
+        for(;;){
+                print("Enabled or Disabled [default %s]: ",
+                        (pw->status & Enabled) ? "Enabled" : "Disabled" );
+                userinput(buf, sizeof(buf));
+                if(strlen(buf) == 0)
+                        break;
+                if(buf[0]=='E' || buf[0]=='e'){
+                        pw->status |= Enabled;
+                        break;
+                }
+                if(buf[0]=='D' || buf[0]=='d'){
+                        pw->status = pw->status & ~Enabled;
+                        break;
+                }
+        }
+        for(;;){
+                print("require STA? [default %s]: ",
+                        (pw->status & STA) ? "yes" : "no" );
+                userinput(buf, sizeof(buf));
+                if(strlen(buf) == 0)
+                        break;
+                if(buf[0]=='Y' || buf[0]=='y'){
+                        pw->status |= STA;
+                        break;
+                }
+                if(buf[0]=='N' || buf[0]=='n'){
+                        pw->status = pw->status & ~STA;
+                        break;
+                }
+        }
+
+        /* free form field */
+        if(isnew)
+                pw->other = nil;
+        print("comments [default = %s]: ", (pw->other == nil) ? "" : pw->other);
+        userinput(buf, 72);  /* 72 comes from password.h */
+        if(buf[0])
+                if((pw->other = strdup(buf)) == nil)
+                        sysfatal("strdup");
+
+        syslog(0, LOG, "CHANGELOGIN for '%s'", pw->id);
+        if(putPW(pw) < 0){
+                print("error writing entry: %r\n");
+                exits("can't write password file");
+        }else{
+                print("change written\n");
+                if(isnew && create(home, OREAD, DMDIR | 0775L) < 0){
+                        print("unable to create %s: %r\n", home);
+                        exits(home);
+                }
+        }
+
+        exits("");
+        return 1;  /* keep  other compilers happy */
+}
+
+
+static void
+userinput(char *buf, int blen)
+{
+        int n;
+
+        while(1){
+                n = read(0, buf, blen);
+                if(n<=0)
+                        exits("read error");
+                if(buf[n-1]=='\n'){
+                        buf[n-1] = '\0';
+                        return;
+                }
+                buf += n;  blen -= n;
+                if(blen<=0)
+                        exits("input too large");
+        }
+}
+
diff --git a/src/cmd/secstore/util.c b/src/cmd/secstore/util.c
t@@ -26,13 +26,3 @@ estrdup(char *s)
                 sysfatal("estrdup");
         return s;
 }
-
-char *
-getpassm(char *prompt)
-{
-        char *p = getpass(prompt);
-
-        if(p == nil || (p = strdup(p)) == nil)
-                sysfatal("getpassm");
-        return p;
-}