mrrl-logincaps

MRRL version of logincaps
git clone https://ccx.te2000.cz/git/mrrl-logincaps
Log | Files | Refs

commit ac19c0ce064461b808ffa9a9a6276ca0ae050591
Author: Jan Pobrislo <ccx@webprojekty.cz>
Date:   Tue,  7 May 2019 03:25:47 +0200

First commit
Diffstat:
Abin/cat_pass_fd | 4++++
Abin/startsshagent | 14++++++++++++++
Abin/startsshagent.passfd | 14++++++++++++++
Abin/zshaskpass | 37+++++++++++++++++++++++++++++++++++++
Abin/zshaskpass_mux | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aetc/loginexec | 45+++++++++++++++++++++++++++++++++++++++++++++
Ahome/.xinitrc | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahome/loginexec | 9+++++++++
Asbin/logincaps | 36++++++++++++++++++++++++++++++++++++
Asbin/su-term.py | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 478 insertions(+), 0 deletions(-)

diff --git a/bin/cat_pass_fd b/bin/cat_pass_fd @@ -0,0 +1,4 @@ +#!/command/execlineb +importas -i LOGINPASS_FD LOGINPASS_FD +fdmove 0 $LOGINPASS_FD +cat diff --git a/bin/startsshagent b/bin/startsshagent @@ -0,0 +1,14 @@ +#!/command/execlineb -s1 +piperw 5 4 +piperw 6 7 +fdmove -c 3 0 +s6-env DISPLAY=tty ZSHASKPASS_READFD=6 ZSHASKPASS_WRITEFD=4 SSH_ASKPASS=/home/ccx/bin/zshaskpass +ssh-agent -a $1 +fdclose 3 +fdclose 4 +fdclose 6 +unexport ZSHASKPASS_READFD +unexport ZSHASKPASS_WRITEFD +if { env DISPLAY=terminal SSH_ASKPASS=/home/ccx/bin/zshaskpass ssh-add -c } +s6-env ZSHASKPASS_PASSFD=7 ZSHASKPASS_PROMPTFD=5 +$@ diff --git a/bin/startsshagent.passfd b/bin/startsshagent.passfd @@ -0,0 +1,14 @@ +#!/command/execlineb -s1 +piperw 5 4 +piperw 6 7 +s6-env DISPLAY=tty ZSHASKPASS_READFD=6 ZSHASKPASS_WRITEFD=4 SSH_ASKPASS=/home/ccx/bin/zshaskpass +ssh-agent -a $1 +fdclose 4 +fdclose 6 +unexport ZSHASKPASS_READFD +unexport ZSHASKPASS_WRITEFD +if { redirfd -r 0 /dev/null env DISPLAY=pipe SSH_ASKPASS=/home/ccx/bin/zshaskpass ssh-add -c } +importas -i LOGINPASS_FD LOGINPASS_FD +fdclose $LOGINPASS_FD +s6-env ZSHASKPASS_PASSFD=7 ZSHASKPASS_PROMPTFD=5 +$@ diff --git a/bin/zshaskpass b/bin/zshaskpass @@ -0,0 +1,37 @@ +#!/bin/zsh +if [[ -n $LOGINPASS_FD && $(</proc/$PPID/cmdline) == ssh-add* ]]; then + exec cat_pass_fd +fi +if [[ $+ZSHASKPASS_READFD -eq 0 || -z $ZSHASKPASS_READFD ]]; then + echo >&2 'ZSHASKPASS_READFD not set' + exit 2 +fi + +if [[ $+ZSHASKPASS_WRITEFD -eq 0 || -z $ZSHASKPASS_WRITEFD ]]; then + echo >&2 'ZSHASKPASS_WRITEFD not set' + exit 2 +fi + +prompt_color=%F{yellow} +accept_color=%F{green} +cancel_color=%F{red} + +exec <& $ZSHASKPASS_READFD +#exec 2>&$ZSHASKPASS_WRITEFD +#exec 2>/run/user/$USER/zshaskpass.new +#mv /run/user/$USER/zshaskpass.new /run/user/$USER/zshaskpass +#set -x +#filan >&2 + +prompt="$* (^D cancels): " +printf "${(%)prompt_color}%s\n" ${(f)prompt} >& $ZSHASKPASS_WRITEFD +read -s PASS +RET=$? +[[ $PASS == $'\0' ]] && RET=1 +if (($RET)); then + printf '%s\n' ${(%)cancel_color}Cancelled >& $ZSHASKPASS_WRITEFD +else + printf '%s\n' ${(%)accept_color}OK >& $ZSHASKPASS_WRITEFD + cat <<<$PASS || exit $? +fi +exit $RET diff --git a/bin/zshaskpass_mux b/bin/zshaskpass_mux @@ -0,0 +1,129 @@ +#!/bin/zsh +if [[ $+ZSHASKPASS_PROMPTFD -eq 0 || -z $ZSHASKPASS_PROMPTFD ]]; then + echo >&2 'ZSHASKPASS_PROMPTFD not set' + exit 2 +fi + +if [[ $+ZSHASKPASS_PASSFD -eq 0 || -z $ZSHASKPASS_PASSFD ]]; then + echo >&2 'ZSHASKPASS_PASSFD not set' + exit 2 +fi + +setopt no_unset warn_create_global +zmodload zsh/zselect + +typeset -g COPROC_OUT fd newfd prompt_end +typeset -a coproc_args +typeset -gA fds + +#if [[ $ZSHASKPASS_PROMPTFD -gt $ZSHASKPASS_PASSFD ]]; then +# COPROC_OUT=$[ $ZSHASKPASS_PROMPTFD + 1 ] +#else +# COPROC_OUT=$[ $ZSHASKPASS_PASSFD + 1 ] +#fi + +prompt_end=%f +coproc_args=( + fdmove -c 2 1 + unexport ZSHASKPASS_PROMPTFD + unexport ZSHASKPASS_PASSFD + unexport SSH_ASKPASS + "$@" +) +if (($+LOGINCAP_READFD)); then + coproc_args=( fdclose $LOGINCAP_READFD unexport LOGINCAP_FD "$coproc_args[@]" ) +fi +if (($+LOGINCAP_WRITEFD)); then + coproc_args=( fdclose $LOGINCAP_WRITEFD unexport LOGINCAP_FD "$coproc_args[@]" ) +fi + +logincap() { + if ! (( $+LOGINCAP_READFD & $+LOGINCAP_WRITEFD )); then + echo "logincap FDs not defined" + return + fi + printf >&$LOGINCAP_WRITEFD '%s\n' "$*" + local line result_color + read -u $LOGINCAP_READFD line + result_color=%F{cyan} + printf '%s\n' ${(%)result_color}$line${(%)prompt_end} +} + +cmd() { + case $1 in + ('') ;; + ('c '*) + logincap ${1#c };; + (off|poweroff) + logincap o;; + (reboot) + logincap b;; + (root) + logincap "terminal rxvt-unicode importas -i PTY_FD PTY_FD s6-envdir /run/user/ccx/X$[ ${TTY#/dev/tty} + 4 ]/env urxvt -pty-fd \$PTY_FD" ;; + (exit) + printf '\n' >/run/user/ccx/X$[ ${TTY#/dev/tty} + 4 ]/fifo;; + (shell) + zsh -i; printf "shell exited with %d\n" $?;; + (*) printf '%s: unknown command\n' ${(qqq)REPLY};; + esac +} + +# typeset -f -t cmd + +coproc "$coproc_args[@]" +trap 'kill %1' EXIT +trap 'kill %1; ssh-add -D; exit' INT HUP TERM QUIT +trap 'printf "\nALRM!\n"' ALRM +(($+TMOUT)) && printf 'TMOUT=%s\n' ${(qqq)TMOUT} +#coproc "$@" +exec {COPROC_OUT}<& p +select_fds=( $ZSHASKPASS_PROMPTFD $COPROC_OUT ) + +while zselect -A fds -r 0 $select_fds; do + if ! (($#jobstates)); then + echo >&2 "Session exited." + exit 0 + fi + for fd in ${(k)fds}; do + if [[ $fds[$fd] != r ]]; then + echo >&2 "Error on filedescriptor $fd" + exit 1 + fi + if [[ $fd == $ZSHASKPASS_PROMPTFD ]]; then + # zshaskpass + if IFS= read -u $fd; then + if [[ $REPLY == *': ' ]]; then + printf '%s' "$REPLY${(%)prompt_end}" + read -s + RET=$? + echo + if (($RET)); then + REPLY=$'\0' + fi + cat <<<$REPLY >& $ZSHASKPASS_PASSFD + else + printf '%s\n' "$REPLY${(%)prompt_end}" + fi + else + echo >&2 "Read from askpass failed" + fi + elif [[ $fd == $COPROC_OUT ]]; then + # coprocess + if IFS= read -u $COPROC_OUT -t; then + if [[ $REPLY == "XFIFO: "* ]]; then + printf '< opening: %s\n' ${REPLY#*: } + exec {newfd}>>${REPLY#*: } + else + printf '> %s\n' $REPLY + fi + fi + else + # stdin + if IFS= read -u $fd -s; then + cmd "$REPLY" + else + echo >&2 "Read from filedescriptor $fd failed" + fi + fi + done +done diff --git a/etc/loginexec b/etc/loginexec @@ -0,0 +1,45 @@ +#!/command/execlineb -P +multisubstitute { + importas HOME HOME + importas USER USER + importas SHELL SHELL +} + +ifthenelse -s { test -d $HOME } { + cd $HOME +} { + cd / +} + +ifelse -n { test -x ${HOME}/loginexec } { + s6-setuidgid $USER + exec -l $SHELL +} + +foreground { printf "%s: running %s\n" /etc/loginexec ${HOME}/loginexec } +piperw 10 8 +piperw 9 11 +background { + fdmove 0 10 + fdmove 1 11 + fdclose 8 + fdclose 9 + fdclose 10 + fdclose 11 + #sleep 365d + importas -D "" LOGINPASS_FD LOGINPASS_FD + ifelse { + test -n $LOGINPASS_FD + } { + fdclose $LOGINPASS_FD + unexport LOGINPASS_FD↵ + /root/logincaps + } + /root/logincaps +} +fdclose 10 +fdclose 11 +export LOGINCAP_READFD 9 +export LOGINCAP_WRITEFD 8 +s6-setuidgid $USER +${HOME}/loginexec diff --git a/home/.xinitrc b/home/.xinitrc @@ -0,0 +1,76 @@ +#!/bin/sh +#set -x + +# Per-user supervision tree is expected in /run/user/$USER/service +# Files specific to each X11 session are put in /run/user/$USER/X<display-number> +DNAME=X${DISPLAY#:} +RUNDIR=/run/user/$USER + +cd "$RUNDIR" || exit $? +mkdir -p "$DNAME" || exit $? +cd "$DNAME" || exit $? + +# Set up lockfile and exit-fifo +touch ./lock || exit $? +exec 3<./lock +flock -w 2 3 || exit $? +if ! test -p fifo; then + mkfifo fifo || exit $? +fi + +# Share the necessary environment variables +# Runscripts should start with: s6-envdir ../../../env +export SX_RC_FIFO=$RUNDIR/$DNAME/fifo +rm -r ./env +s6-dumpenv ./env || exit $? +# mkdir -p ./env || exit $? +# printf '%s' >../env/"$DNAME"/DISPLAY "$DISPLAY" || exit $? +# printf '%s' >../env/"$DNAME"/XAUTHORITY "$XAUTHORITY" || exit $? + +# Check if s6-rc was already initialized +if test -e ./s6-rc; then + # Make sure the session is down + s6-rc -l "$PWD"/s6-rc -da -t 1500 change + + # Update the session so it uses the current compiled state + s6-rc-update -l "$PWD"/s6-rc "$(s6-linkname -f "$HOME"/s6-rc/xsession/compiled)" || exit $? +else + # Check for any leftover service symlinks which would prevent s6-rc-init from succeeding + if stat >/dev/null ../service/"$DNAME"-*; then + for sv in ../service/"$DNAME"-*; do + dst=$(s6-linkname -f "$sv") + rm "$sv" + test -e "$dst/supervise" && s6-svc -wD -T 300 -x "$dst" + done + fi + + if ! s6-rc-init -p "$DNAME"- -d -l "$PWD"/s6-rc -c "$HOME"/s6-rc/xsession/compiled "$RUNDIR"/service; then + # s6-rc-init failed, clean up livedir symlink so it's not considered initialized successfully + rm -f ./s6-rc + exit 1 + fi +fi + +set -x +# Start up the session +s6-svscanctl -a "$RUNDIR"/service || exit $? +s6-rc -l "$PWD"/s6-rc -- change ok-all || exit $? + +# Run until we get message on fifo to exit or until this process dies +# Cleanup process is forked until parent dies and then stops s6-rc +exec env PWD="$PWD" execlineb -c ' +importas -i PWD PWD +pipeline -w { + foreground { redirfd -w 1 /dev/null cat } + fdclose 0 + foreground { s6-echo "X session terminated" } + if -n { s6-rc -l $PWD/s6-rc -da change } + foreground { s6-echo "`s6-rc -da change` failed" } +} +cat fifo +' +#foreground { +# importas -i SX_RC_FIFO SX_RC_FIFO +# fdmove 1 2 +# s6-echo XFIFO: $SX_RC_FIFO +#} diff --git a/home/loginexec b/home/loginexec @@ -0,0 +1,9 @@ +#!/bin/zsh -l +set -x +if [[ $TTY = /dev/tty[1-4] ]]; then + X=$[ ${TTY#/dev/tty} + 4 ] + exec startsshagent.passfd /run/user/$USER/ssh_agent.${${TTY#/dev/}//\//.} =zshaskpass_mux xinit -- :$X tty$X -quiet +else + exec ssh-agent -a /run/user/$USER/ssh_agent.${${TTY#/dev/}//\//.} /bin/execlineb -c \ + 'if { redirfd -r 0 /dev/null env DISPLAY=pipe SSH_ASKPASS=/home/ccx/bin/cat_pass_fd ssh-add } zsh -l' +fi diff --git a/sbin/logincaps b/sbin/logincaps @@ -0,0 +1,36 @@ +#!/bin/zsh +setopt no_unset warn_create_global +#set -x + +trap 'printf "\nlogincaps: ALRM!\n"' ALRM + +pretendrun() { : "$@" } +typeset -f -t pretendrun + +main() { + local line term_cmd REPLY + while read line; do + case $line in + (o) printf >&2 "%s\n" "Would power-off." + pretendrun s6-poweroff + printf 'OK\n' + ;; + (b) printf >&2 "%s\n" "Would reboot." + pretendrun s6-reboot + printf 'OK\n' + ;; + (terminal *) + term_cmd=( "${(Q@)${(z)${line#terminal }}}" ) + printf >&2 "%s\n" "Would launch root terminal." + pretendrun /root/su-term.py -d 1 -u $USER -- "$term_cmd[@]" <&2 + #{ /root/su-term.py -d 1 -u $USER -- "$term_cmd[@]" <&2 &! + #} | read + printf 'OK\n' + ;; + + (*) printf 'ECMD\n';; + esac + done +} + +main diff --git a/sbin/su-term.py b/sbin/su-term.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python + +from __future__ import ( + generators, division, absolute_import, with_statement, print_function +) +import sys +import os +import os.path +import pwd +import spwd +import crypt +import getpass +import argparse +import re + +parser = argparse.ArgumentParser(description="Check root password and run terminal as given user with root shell") +parser.add_argument('-u', '--user', required=True, help="User name or ID to run terminal as") +parser.add_argument('-g', '--group', type=int, help="Group ID to run terminal as") +parser.add_argument('-d', '--notify-fd', type=int, help="Write newline to this filedescriptor after reading password") +parser.add_argument('term_env', help='The TERM variable used by the shell') +parser.add_argument('exe', nargs='+', help='Terminal application to execute') + + +def exec_terminal(uid, gid, home, terminal, term_env, shell, shell_workdir): + assert isinstance(uid, int) + assert isinstance(gid, int) + assert isinstance(home, str) + assert isinstance(terminal, (list, tuple)) + assert all(isinstance(s, str) for s in terminal) + assert len(terminal) + assert isinstance(shell, str) + master, slave = os.openpty() + if os.fork(): + # parent + os.close(slave) + os.setgid(gid) + os.setuid(uid) + os.chdir(home) + env = dict(os.environ) + env['PTY_FD'] = str(master) + env['HOME'] = home + if '/' in terminal[0]: + os.execve(terminal[0], terminal, env) + else: + # TODO: proper search path + os.execve('/bin/exec', ['exec'] + terminal, env) + else: + # child + os.close(master) + os.dup2(slave, 0) + os.dup2(slave, 1) + os.dup2(slave, 2) + os.close(slave) + os.setsid() + os.chdir(shell_workdir) + env = dict(os.environ) + env['USER'] = 'root' + env['TERM'] = term_env + env['HOME'] = shell_workdir + env['SHELL'] = shell + env.pop('LOGNAME', None) + os.execve(shell, ['-' + os.path.basename(shell)], env) + + +def constant_time_compare(val1, val2): + """ + Returns True if the two strings are equal, False otherwise. + + The time taken is independent of the number of characters that match. + + For the sake of simplicity, this function executes in constant time only + when the two strings have the same length. It short-circuits when they + have different lengths. + """ + if len(val1) != len(val2): + return False + result = 0 + for x, y in zip(val1, val2): + result |= ord(x) ^ ord(y) + return result == 0 + + +def main(): + args = parser.parse_args() + if re.match('[0-9]+', args.user): + uid = int(args.user) + pwent = pwd.getpwuid(uid) + else: + pwent = pwd.getpwnam(args.user) + uid = pwent.pw_uid + gid = pwent.pw_gid if args.group is None else int(args.group) + root_hash = spwd.getspnam('root').sp_pwd + root_pwent = pwd.getpwnam('root') + p = getpass.getpass() + if constant_time_compare(root_hash, crypt.crypt(p, root_hash)): + # OK + if args.notify_fd is not None: + os.write(args.notify_fd, '\n') + exec_terminal( + uid=uid, + gid=gid, + home=pwent.pw_dir, + terminal=args.exe, + term_env=args.term_env, + shell=root_pwent.pw_shell, + shell_workdir=root_pwent.pw_dir, + ) + else: + sys.stderr.write("Incorrect password\n") + sys.exit(2) + + +if __name__ == '__main__': + main()