implement Sshkeys; include "sys.m"; sys: Sys; sprint: import sys; include "draw.m"; draw: Draw; include "arg.m"; include "bufio.m"; bufio: Bufio; Iobuf: import bufio; include "env.m"; env: Env; include "string.m"; str: String; include "tk.m"; tk: Tk; include "tkclient.m"; tkclient: Tkclient; include "util0.m"; util: Util0; l2a, rev, fail, warn: import util; Sshkeys: module { init: fn(ctxt: ref Draw->Context, argv: list of string); }; dflag: int; top: ref Tk->Toplevel; wmctl: chan of string; tkcmds0 := array[] of { "frame .f", "text .f.t -yscrollcommand {.f.scrolly set} -wrap word", ".f.t insert 1.0 'Waiting for request...", "scrollbar .f.scrolly -command {.f.t yview}", "frame .c", "button .c.deny -command {send cmd deny} -text 'Deny", "button .c.once -command {send cmd once} -text 'Allow once", "button .c.add -command {send cmd add} -text 'Allow & Store key", "button .c.replace -command {send cmd replace} -text 'Allow & Replace existing key(s)", "pack .f.scrolly -side left -fill y", "pack .f.t -fill both -expand 1", "pack .f -fill both -expand 1", "pack .c -fill x", "pack propagate . 0", ". configure -width 600 -height 300", }; init(ctxt: ref Draw->Context, args: list of string) { sys = load Sys Sys->PATH; if(ctxt == nil) fail("no window context"); draw = load Draw Draw->PATH; arg := load Arg Arg->PATH; bufio = load Bufio Bufio->PATH; str = load String String->PATH; env = load Env Env->PATH; tk = load Tk Tk->PATH; tkclient = load Tkclient Tkclient->PATH; util = load Util0 Util0->PATH; util->init(); arg->init(args); arg->setusage(arg->progname()+" [-d]"); while((c := arg->opt()) != 0) case c { 'd' => dflag++; * => arg->usage(); } args = arg->argv(); if(args != nil) arg->usage(); fio := sys->file2chan("/chan", "sshkeys"); if(fio == nil) fail(sprint("/chan/sshkeys: %r")); tkclient->init(); (top, wmctl) = tkclient->toplevel(ctxt, "", "Sshkeys", Tkclient->Appl); tkcmdchan := chan of string; tk->namechan(top, tkcmdchan, "cmd"); for(i := 0; i < len tkcmds0; i++) tkcmd(tkcmds0[i]); tkclient->onscreen(top, nil); tkclient->startinput(top, "kbd"::"ptr"::nil); tkclient->wmctl(top, "task"); fiowrite := fio.write; request: array of byte; respondc: Sys->Rwrite; keys, existing: list of ref Entry; # existing keys, possibly to be replaced new: ref Entry; for(;;) alt { s := <-top.ctxt.kbd => tk->keyboard(top, s); s := <-top.ctxt.ptr => tk->pointer(top, *s); s := <-top.ctxt.ctl or s = <-top.wreq => tkclient->wmctl(top, s); menu := <-wmctl => case menu { "exit" => if(respondc != nil) respondc <-= (-1, "host key denied"); return; * => tkclient->wmctl(top, menu); } cmd := <-tkcmdchan => if(respondc != nil) { case cmd { "deny" => respondc <-= (-1, "host key denied"); "once" or "add" or "replace" => respondc <-= (len request, nil); } case cmd { "add" => f := keysfile(); fd := sys->open(f, Sys->OWRITE); if(fd == nil || sys->seek(fd, big 0, Sys->SEEKEND) < big 0 || sys->fprint(fd, "%s\n", new.text()) < 0) warn(sprint("adding key to %q: %r", f)); "replace" => keys = filter(keys, existing); s := ""; for(; keys != nil; keys = tl keys) s += (hd keys).text()+"\n"; s += new.text()+"\n"; f := keysfile(); fd := sys->open(f, Sys->OWRITE|Sys->OTRUNC); if(fd == nil || sys->fprint(fd, "%s", s) < 0) warn(sprint("rewriting %q: %r", f)); } case cmd { "deny" or "once" or "add" or "replace" => tkcmd(".f.t delete 1.0 end"); tkcmd(".f.t insert 1.0 'Waiting for request..."); tkcmd("pack forget .c.deny .c.once .c.add .c.replace"); tkcmd("update"); tkclient->wmctl(top, "task"); fiowrite = fio.write; request = nil; respondc = nil; keys = nil; new = nil; } } (nil, nil, nil, rc) := <-fio.read => if(rc != nil) rc <-= (nil, "permission denied"); (nil, buf, nil, wc) := <-fiowrite => if(wc == nil) break; t := l2a(str->unquoted(string buf)); if(len t != 4) { wc <-= (-1, "bad argument"); break; } e := ref Entry (t[0], t[1], t[2], t[3]); err: string; (keys, err) = readkeys(); if(err != nil) { wc <-= (-1, err); break; } (nil, matches, badkeys, otheralgs) := split(keys, e); if(matches != nil) { wc <-= (len buf, nil); break; } msg: string; existing = nil; if(badkeys != nil) { existing = badkeys; msg = sprint("Conflicting host key for address %q, algorithm %#q!\nThis could be a man-in-the-middle attack!", e.addr, e.alg); tkcmd("pack .c.deny .c.once .c.add .c.replace -side left -fill x -expand 1"); } else if(otheralgs != nil) { existing = otheralgs; msg = sprint("Received host key for address %q with different algorithm then recorded.", e.addr); tkcmd("pack .c.deny .c.once .c.add .c.replace -side left -fill x -expand 1"); } else { msg = sprint("First connection to address %q.\nPlease verify this is the correct key.", e.addr); tkcmd("pack .c.deny .c.once .c.add -side left -fill x -expand 1"); } msg += sprint("\n\nRemote claims:\nAddress: %s\nAlgorithm: %s\nFingerprint: %s\nHost key:\n%s\n\n", e.addr, e.alg, e.fp, e.hostkey); if(existing != nil) { msg += sprint("Existing keys for address %q are:\n", e.addr); for(r := existing; r != nil; r = tl r) { ee := hd r; msg += sprint("Algorithm: %s\nFingerprint: %s\nHost key:\n%s\n\n", ee.alg, ee.fp, ee.hostkey); } } tkcmd(".f.t delete 1.0 end; .f.t insert 1.0 '"+msg); tkcmd("update"); tkclient->wmctl(top, "unhide"); new = e; request = buf; respondc = wc; fiowrite = chan of (int, array of byte, int, Sys->Rwrite); } } keysfile(): string { return env->getenv("home")+"/lib/sshkeys"; } Entry: adt { addr, alg, fp, hostkey: string; text: fn(e: self ref Entry): string; }; Entry.text(e: self ref Entry): string { return sprint("%q %q %q %q", e.addr, e.alg, e.fp, e.hostkey); } filter(l: list of ref Entry, del: list of ref Entry): list of ref Entry { r: list of ref Entry; for(; l != nil; l = tl l) if(!has(del, hd l)) r = hd l::r; return rev(r); } has[T](l: list of T, e: T): int { for(; l != nil; l = tl l) if(hd l == e) return 1; return 0; } readkeys(): (list of ref Entry, string) { f := keysfile(); b := bufio->open(f, Bufio->OREAD); if(b == nil) return (nil, nil); n := 0; l: list of ref Entry; for(;;) { s := b.gets('\n'); if(s == nil) break; n++; if(s[len s-1] == '\n') s = s[:len s-1]; t := l2a(str->unquoted(s)); if(len t != 4) return (nil, sprint("%s:%d: malformed line", f, n)); e := ref Entry (t[0], t[1], t[2], t[3]); l = e::l; } return (rev(l), nil); } split(l: list of ref Entry, ee: ref Entry): (list of ref Entry, list of ref Entry, list of ref Entry, list of ref Entry) { otheraddr, match, badkey, otheralg: list of ref Entry; for(; l != nil; l = tl l) { e := hd l; if(e.addr != ee.addr) otheraddr = e::otheraddr; else if(e.alg != ee.alg) otheralg = e::otheralg; else if(e.fp != ee.fp || e.hostkey != ee.hostkey) badkey = e::badkey; else match = e::match; } return (rev(otheraddr), rev(match), rev(badkey), rev(otheralg)); } tkcmd(s: string): string { r := tk->cmd(top, s); if(r != nil && r[0] == '!') warn(sprint("tkcmd: %q: %s", s, r)); return r; } say(s: string) { if(dflag) warn(s); }