/* [ʞ] xpriv.c * ~ lexi hale * $ cc -Ofast xpriv.c -lrt -lutil -lX11 -oxpriv * © affero general public license * xpriv.c is a tool for a very specific use case i have. * for security's sake, i don't tie my ssh keys to my * login session. when i intend to use them, i spawn a * special terminal and load the keys into memory, then * exit that terminal when i'm done using them. however, * i often lose track of or accidentally kill that win- * dow, despite adding a visual cue to its prompt. * safekill.c was one half of my attempt to address this * problem; this is the other. * * xpriv performs several different tasks to accomplish a * single purpose: i can hit a single keystroke that * will either conjure up a new privileged session, or * switch to one that's already active if it exists. it * does this by first checking for the existence of a * shared memory segment. if it doesn't find it, it * starts a new session; if it *does* find it, it * retrieves the X11 window ID from that shared memory * and sends a _NET_ACTIVE_WINDOW client message to the * root X window. the window manager interprets the * message, activating the window. * * the flag -k can also be passed, in which case the * utility instructs the running process to liquidate its * subprocesses and exit itself. * * if the shared memory does not exist, xpriv creates a * new instance of urxvt. this instance is told to run * the command “xpriv -a” instead of the user’s normal * shell. the -a flag instructs xpriv to get the terminal * window’s ID from the $WINDOWID environment variable * which urxvt sets. after this, a ssh-agent process is * launched. xpriv waits until it has opened a socket and * then runs ssh-add without parameters to add the user's * default keys to the session. * * after a success key-add has been confirmed, xpriv * marks the window as “vital” by setting the X property * “_k_vital” on the window. if the login fails or does * not complete, safekill.c will still terminate it at * any time. the vital flag is removed as soon as the * controlling shell terminates; it does *not* remain for * the lifetime of the window, so a "temporary" session * can be created in the current terminal by calling * `xpriv -a` directly. * * xpriv does its best to clean up after itself, * killing all sensitive processes and their children and * removing the shmem segment when it is no longer in * use, even if exits somewhat abnormally. if you have to * kill -9 xpriv at any point tho, you can make it work * again (on linux) with * rm /dev/shm/k.xpriv:(xpriv binary basename) * * you can have multiple xpriv sessions by creating soft- * links to the binary with a different name for each. TODO send signal to urxvtd directly instead of launching urxvtc as a separate process TODO make shell & commands performed configurable, with flags to control or supplant ssh-agent TODO add a randomizer call that works on BSD TODO document flags TODO implement/remove lock flag TODO add flag to bring window to current desktop TODO rewrite using sysv shmem */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _RAND_SYSCALL # include # define getrandom(a,b,c) syscall(SYS_getrandom,a,b,c) /* this is necessary on certain platforms due to * certainly ungodly libc issues, i think. */ #endif #ifdef _SHM_LINUX /* xpriv was originally written with linux shared * memory in mind, because i'm an idiot. i've since * redesigned it to use the superior old sysv shm * api that works on every other unix, not just * linux, but if for some ungodly reason you want * to use the linux one, then just pass -D_SHM_LINUX */ # define shmem_prefix "/k.xpriv:" # define shmlinux(...) __VA_ARGS__ # define shmsysv(...) #else # define shmlinux(...) # define shmsysv(...) __VA_ARGS__ #endif enum /* constants */ { false = 0, true = 1, shmsysv(shmem_key = 0x53373EC3,) /* ye olde magique numbre */ }; typedef _Bool bool; enum mode { mode_usage, mode_go, mode_register, mode_kill, mode_lock }; enum res { ok, fail_parse, fail_arg, fail_opt, fail_shm, fail_mem, fail_pty, fail_nop, fail_win, fail_wid, fail_x11}; enum res bad(enum res code) { if (code == ok) return ok; write(1,"\e[1m|e|\e[0m ",12); const char* msg; uint8_t len; switch(code) { #define say(x) { msg = (x "\n"); len = (sizeof x) + 1; break; } case fail_parse: say("could not parse command-line options; pass -u for usage"); case fail_arg : say("invalid argument provided"); case fail_opt : say("no such option"); case fail_shm : say("instance already running"); case fail_mem : say("could not map memory"); case fail_pty : say("could not alloc pty"); case fail_nop : say("no instance running"); case fail_win : say("cannot open display"); case fail_wid : say("WINDOWID not defined"); case fail_x11 : say("X server refuses to handle request"); #undef say } write(1, msg, len); return code; } struct signal { Window wid; pid_t pid; pid_t agent; enum mode op; }*global; char* itoa(unsigned long i, char* buf, size_t bufsz) { char* cur = buf + bufsz; while (cur >= buf) { *--cur='0' + i % 10; if (i < 10) break; else i /= 10; } return cur; } bool run; struct termios initial_state; void sigusr(int a) { if (global -> op == mode_kill) run = false; } void sigterm(int a) { run = false; } void spawn(pid_t ssha, const char* const sockn) { if (ssha = fork()) { char pid_s_buf[16]; char* pid_s = itoa(ssha, pid_s_buf, sizeof(pid_s_buf)); while (access(sockn, F_OK)); // avoid nasty race condition setenv("SSH_AGENT_PID", pid_s, true); setenv("SSH_AUTH_SOCK", sockn, true); global -> agent = ssha; } else { close(1); close(0); execlp("ssh-agent","ssh-agent","-D","-a",sockn,0); } } enum res register_window(shmlinux(const char* const id,) bool weak, const char* name) { // the id field denotes the path to the shared memory // in use, and allows the user to have multiple // contexts by creating aliases to the binary struct signal* s; shmlinux ({ int fd = shm_open(id, O_CREAT | O_EXCL | O_RDWR, 0600); ftruncate(fd, sizeof(struct signal)); if (fd == -1) return fail_shm; s = mmap(0, sizeof(struct signal), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if(s == MAP_FAILED) return fail_mem; else global = s; }) shmsysv (int shmem; shmem = shmget(shmem_key, sizeof (struct signal), IPC_CREAT | 0777); if (shmem == -1) return fail_shm; s = shmat(shmem, 0,0); if (s == (void*)-1) return fail_mem; else global = s; ) Display* xdpy = XOpenDisplay(NULL); Atom xvital; /* x11 */ { if (xdpy == NULL) return fail_win; Window win; const char* winid_s; if (!(winid_s = getenv("WINDOWID"))) return fail_wid; win=(Window)strtol(winid_s,NULL,10); if(weak == false) xvital = XInternAtom(xdpy,"_k_vital",false); s -> wid = win; } pid_t child; if(child = fork()) { s -> pid = child; sigset_t mask, oldmask; sigemptyset(&mask); sigaddset(&mask, SIGUSR1); sigprocmask(SIG_BLOCK, &mask, &oldmask); signal(SIGUSR1, sigusr); signal(SIGTERM, sigterm); signal(SIGCHLD, sigterm); signal(SIGHUP, sigterm); run = true; while(run == true) { sigsuspend(&oldmask); } if(s -> op == mode_kill) { kill(s -> pid, SIGTERM); } kill(s -> agent, SIGTERM); sigprocmask(SIG_BLOCK, &mask, NULL); shmlinux(shm_unlink(id)); shmsysv({ struct shmid_ds bleh; /* ?? */ shmctl(shmem, IPC_RMID, &bleh); }) if (!weak) { XDeleteProperty(xdpy,s -> wid,xvital); XSync(xdpy,false); } XCloseDisplay(xdpy); } else { // now we start ssh-agent and set the proper environment // variables pid_t ssha; if (name == NULL) { /* messy part */ const char* tmp; //tmpdir defined? if (!(tmp = getenv("XDG_RUNTIME_DIR"))) if (!(tmp = getenv("TMPDIR"))) tmp = "/tmp"; size_t tmpsz = strlen(tmp); char* sockn = malloc(tmpsz + 1 + sizeof "ssh." + 11); strcpy(sockn, tmp); sockn[tmpsz] = '/'; strcpy(sockn+tmpsz+1,"ssh."); // first, gen a random identifier so we have the ability // to know where the socket winds up uint8_t* rndid = sockn+tmpsz+1+4; getrandom(rndid, 11, 0); // assuming ascii… for(uint8_t*i=rndid;i '9') *i += 7; if (*i > 'Z') *i += ('a' - 'Z'); } name = sockn; } spawn(ssha, name); pid_t sad; int p; if (sad = fork()) { int added; waitpid(sad, &added, 0); if (added == 0) { if (weak == false) { XChangeProperty(xdpy,s -> wid,xvital,xvital,8,PropModeReplace,"\01", 1); XSync(xdpy,false); } write(1,"\033c",3); execlp("fish","fish",NULL); } } else { execlp("ssh-add","ssh-add",NULL); } } return ok; } enum res kill_window(shmlinux(const char* id)) { struct signal* s; shmlinux({ int fd = shm_open(id, O_RDWR, 0600); if (fd == -1) return fail_nop; s = mmap(0, sizeof(struct signal), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if(s == MAP_FAILED) return fail_mem; }) shmsysv (int shmem; { shmem = shmget(shmem_key, sizeof(struct signal), 0); if (shmem == -1) return fail_nop; s = shmat(shmem,0,0); if (s == (void*)-1) return fail_mem; }) shmsysv({ struct shmid_ds bleh; /* ?? */ shmctl(shmem, IPC_RMID, &bleh); }) s -> op = mode_kill; kill(s -> pid, SIGUSR1); return ok; } enum res activate_window(Window w) { Display* dpy = XOpenDisplay(NULL); long mask = SubstructureRedirectMask | SubstructureNotifyMask; XEvent ev = { .xclient.type = ClientMessage, .xclient.serial = 0, .xclient.send_event = True, .xclient.message_type = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False), .xclient.display = dpy, .xclient.window = w, .xclient.format = 32, .xclient.data.l[0] = 2, .xclient.data.l[1] = CurrentTime, .xclient.data.l[2] = 0, }; if(!XSendEvent(dpy, DefaultRootWindow(dpy), False, mask, &ev)) return fail_x11; XSync(dpy,false); return ok; } int main(int sz, char** argv) { char* binname = "xpriv"; if(sz > 0) binname = argv[0]; enum mode op = mode_go; bool init_weak = false; const char* init_named = NULL; for(int i = 1; iwid)); } } else if (op == mode_register) return bad(register_window(shmlinux(shid,) init_weak,init_named)); else if (op == mode_kill) return bad(kill_window(shmlinux(shid))); else if (op == mode_usage) { write(1,"\e[1musage:\e[0m ",15); write(1, argv[0], strlen(argv[0])); write(1, " [-aklw [arg]]\n",16); return fail_parse; } }