#!/usr/bin/env mksh
rcsid='$MirOS: contrib/hosted/tg/code/kwalletcli/pinentry-kwallet,v 1.21 2025/12/14 08:06:03 tg Exp $'
licence='
# Copyright © 2009, 2010, 2011, 2025
#	mirabilos <m$(date +%Y)@mirbsd.de>
#
# Provided that these terms and disclaimer and all copyright notices
# are retained or reproduced in an accompanying document, permission
# is granted to deal in this work without restriction, including un‐
# limited rights to use, publicly perform, distribute, sell, modify,
# merge, give away, or sublicence.
#
# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
# the utmost extent permitted by applicable law, neither express nor
# implied; without malicious intent or gross negligence. In no event
# may a licensor, author or contributor be held liable for indirect,
# direct, other damage, loss, or other issues arising in any way out
# of dealing in the work, even if advised of the possibility of such
# damage or existence of a defect, except proven that it results out
# of said person’s immediate fault when using the work as intended.'

set -U

# or e.g. en_US.UTF-8 or en_US.utf8 or the likes, depends on your OS
# choose one that is always available and uses UTF-8/CESU-8 encoding
substlocale=C.UTF-8	# sync with manpage ENVIRONMENT section

iodebug=0
iodp=~/pinentry-kwallet.debug
if (( iodebug )); then
	print "\n$$ === new $(date)" >>"$iodp"
	chmod 0600 "$iodp"
fi
function io_p_in {
	local io_line
	IFS= read -r io_line || return $?
	(( iodebug )) && print -r -- "$$ <p $io_line" >>"$iodp"
	eval $1='${io_line%
}'
	return 0
}
function io_s_in {
	local io_line
	IFS= read -pr io_line || return $?
	(( iodebug )) && print -r -- "$$ <s $io_line" >>"$iodp"
	eval $1=\$io_line
	return 0
}
function io_p_out {
	(( iodebug )) && print -r -- "$$ >p $(print "$@")" >>"$iodp"
	print "$@"
}
function io_s_out {
	(( iodebug )) && print -r -- "$$ >s $(print "$@")" >>"$iodp"
	print -p "$@"
}
function log {
	(( iodebug )) && print -r -- "$$ LOG $*" >>"$iodp"
}

if [[ -n $PINENTRY_KWALLET ]]; then
	io_p_out 'ERR 86 trying to call me recursively'
	while io_p_in line; do
		io_p_out 'ERR 86 trying to call me recursively'
	done
	exit 1
fi

quiet=0
set -A args
last=
have_lc_type=false
i=0
inarg=false
while (( $# )) || $inarg; do
	if ! $inarg; then
		arg=$1
		shift
		log "argv[$((++i))]='${arg//\'/\'\\\'\'}'"
		oarg=$arg
		if [[ $arg = *=* ]]; then
			gotval=true
			val=${arg#*=}
			arg=${arg%%=*}
		else
			gotval=false
		fi
	fi
	if $inarg; then
		x=${sarg#?}
		arg=${sarg%%"$x"}
		sarg=$x
		case $arg in
		(a)	passon=true  needval=true  arg=--ttyalert ;;
		(C)	passon=false needval=true  arg=--lc-ctype ;;
		(c)	passon=true  needval=true  arg=--colors ;;
		(d)	passon=true  needval=false arg=--debug ;;
		(D)	passon=true  needval=true  arg=--display ;;
		(g)	passon=true  needval=false arg=--no-global-grab ;;
		(h|\?)	passon=false needval=false arg=--help ;;
		(M)	passon=true  needval=true  arg=--lc-messages ;;
		(N)	passon=true  needval=true  arg=--ttytype ;;
		(o)	passon=true  needval=true  arg=--timeout ;;
		(q)	passon=false needval=false ;;
		(T)	passon=true  needval=true  arg=--ttyname ;;
		(V)	passon=false needval=false ;;
		(W)	passon=true  needval=true  arg=--parent-wid ;;
		(*)
			print -ru2 "W: unknown option char '$arg' in '$oarg'"
			args[${#args[*]}]=-$arg
			[[ -n $sarg ]] || inarg=false
			continue ;;
		esac
		gotval=false
		$needval && if [[ -n $sarg ]]; then
			gotval=true
			val=$sarg
			sarg=
		fi
		[[ -n $sarg ]] || inarg=false
	elif [[ $oarg = -- || $oarg = - ]]; then
		for arg in "$oarg" "$@"; do
			args[${#args[*]}]=$arg
		done
		break
	elif [[ $arg = --c?(olors|olor|olo|ol|o) ]]; then
		passon=true  needval=true  arg=--colors
	elif [[ $oarg = --de?(bug|bu|b) ]]; then
		passon=true  needval=false arg=--debug
	elif [[ $arg = --di?(splay|spla|spl|sp|s) ]]; then
		passon=true  needval=true  arg=--display
	elif [[ $arg = --lc-@(ctype|ctyp|cty|ct|c|type|typ|ty|t) ]]; then
		passon=false needval=true  arg=--lc-ctype
	elif [[ $arg = --lc-m?(essages|essage|essag|essa|ess|es|e) ]]; then
		passon=true  needval=true  arg=--lc-messages
	elif [[ $oarg = --n?(o-global-grab|o-global-gra|o-global-gr|o-global-g|o-global-|o-global|o-globa|o-glob|o-glo|o-gl|o-g|o-|o) ]]; then
		passon=true  needval=false arg=--no-global-grab
	elif [[ $arg = --p?(arent-wid|arent-wi|arent-w|arent-|arent|aren|are|ar|a) ]]; then
		passon=true  needval=true  arg=--parent-wid
	elif [[ $arg = --ti?(meout|meou|meo|me|m) ]]; then
		passon=true  needval=true  arg=--timeout
	elif [[ $arg = --ttya?(lert|ler|le|l) ]]; then
		passon=true  needval=true  arg=--ttyalert
	elif [[ $arg = --ttyn?(ame|am|a) ]]; then
		passon=true  needval=true  arg=--ttyname
	elif [[ $arg = --ttyt?(ype|yp|y) ]]; then
		passon=true  needval=true  arg=--ttytype
	elif [[ $oarg = --@(help|version|warranty|dump-options) ]]; then
		passon=false needval=false arg=$oarg
	elif [[ $oarg = --* || $oarg != -* ]]; then
		(( quiet )) || print -ru2 -- "W: unknown argument: $oarg"
		args[${#args[*]}]=$oarg
		continue
	else
		sarg=${oarg#-}
		inarg=true
		continue
	fi
	$needval && if ! $gotval; then
		if (( $# == 0 )); then
			print -ru2 "W: missing option argument for '$arg'"
		else
			val=$1
			shift
			log "argv[$((++i))]='${val//\'/\'\\\'\'}'"
			gotval=true
		fi
	fi
	$gotval && case $arg in
	(--display)	export DISPLAY=$val ;;
	(--lc-ctype)	export LC_CTYPE=$val ;;
	(--lc-messages)	export LC_MESSAGES=$val ;;
	(--ttyname)	export GPG_TTY=$val ;;
	(--ttytype)	export GPG_TERM=$val ;;
	esac
	if $passon; then
		args[${#args[*]}]=$arg
		$gotval && args[${#args[*]}]=$val
		continue
	fi
	case $arg in
	(--lc-ctype)
		$gotval && have_lc_type=true ;;
	(--help)
		print -ru2 -- "$rcsid"
		print "Usage: pinentry-kwallet [options]"
		exit 0 ;;
	(--version)
		print -r -- "$rcsid"
		exit 0 ;;
	(--warranty)
		print -r -- "${licence#?}"
		exit 0 ;;
	(--dump-options)
		cat <<-'EOF'
			--debug
			--display
			--ttyname
			--ttytype
			--lc-ctype
			--lc-messages
			--timeout
			--no-global-grab
			--parent-wid
			--colors
			--ttyalert
			--dump-options
			--help
			--version
			--warranty
		EOF
		;;
	(q)
		quiet=1 ;;
	(V)
		(( quiet )) || print -ru2 -- "$rcsid"
		exit 0 ;;
	(*)
		print -ru2 "E: internal error: no-passon arg '$arg'"
		typeset -p
		exit 255 ;;
	esac
done

if [[ -n $PINENTRY_KWALLET_OVERRIDE ]]; then
	log "overriding \$PINENTRY with '$PINENTRY_KWALLET'"
	PINENTRY=$PINENTRY_KWALLET_OVERRIDE
fi

if [[ -z $DISPLAY ]]; then
	log "since DISPLAY is not set, replacing with: ${PINENTRY:-pinentry}"
	PINENTRY_KWALLET=set exec "${PINENTRY:-pinentry}" "${args[@]}"
fi

x_dsctxt=
x_prompt=
x_errtxt=

function getit {
	local type=$1 key=〈${x_prompt}〉$x_dsctxt pw rv tw=0 d errcnt blst=0

	copyline=0
	# the errcnt handling is a little tricky, because GnuPG v2 does
	# not reuse the pinentry session (suckers, unable to... *rant*)
	if pw=$(kwalletcli -q -f pinentry-kwallet -e "$type-B-$key") && \
	    [[ $pw = yes* ]]; then
		log "blacklisted"
		blst=1
	elif pw=$(kwalletcli -q -f pinentry-kwallet -e "$type-e-$key"); then
		log "read errcnt: '$pw'"
		set -A errcnt -- $pw
		d=$(date -u +'%s')
		(( errcnt[0] < (d - 15) )) && errcnt[1]=0
	else
		log "read errcnt failed"
	fi
	(( blst )) || [[ -z $x_errtxt ]] || (( errcnt[1]++ ))
	(( blst )) || if (( errcnt[1] )); then
		errcnt[0]=${d:-$(date -u +'%s')}
		kwalletcli -q -f pinentry-kwallet -e "$type-e-$key" \
		    -p "${errcnt[*]}"
		log "write errcnt: '${errcnt[*]}' -> $?"
	fi
	(( blst )) || if (( errcnt[1] < 2 )); then
		pw=$(kwalletcli -q -f pinentry-kwallet -e "$type-v-$key")
		rv=$?
		log "read pass $rv: '$pw'"
		case $type:$rv {
		(bool:0)
			if [[ $pw = \
			    @(1|-1|[Tt][Rr][Uu][Ee]|[Yy][Ee][Ss]) ]]; then
				io_p_out OK
				return
			elif [[ $pw = \
			    @(0|[Ff][Aa][Ll][Ss][Ee]|[Nn][Oo]) ]]; then
				io_p_out 'ERR 83886194 not confirmed'
				return
			fi
			;;
		(pass:0)
			[[ -n $pw ]] && io_p_out -r "D $pw"
			io_p_out OK
			return
			;;
		}
	fi
	if (( !have_sub )); then
		io_p_out 'ERR 85 no coprocess'
		return
	fi
	if [[ $type = bool ]]; then
		io_s_out CONFIRM
		io_s_in resp
		case $resp {
		(OK@(| *))
			pw=1
			tw=1
			;;
		(ERR\ @(128|83886194|83886179)@(| *))
			# 128 = not confirmed (hardy)
			# 83886194 = not confirmed (wheezy) = 0x05000000 | 114
			# 83886179 = canceled (wheezy) = 0x05000000 | 99
			pw=0
			tw=1
			;;
		}
	else
		io_s_out GETPIN
		io_s_in resp
		pw=
		#XXX normally, read until OK|ERR
		if [[ $resp = @(D )* ]]; then
			pw=${resp#D }
			io_s_in resp
		fi
		[[ $resp = OK@(| *) ]] && tw=1
	fi
	(( tw && !blst )) && if kwalletcli_getpin -q -b \
	    -t "Do you want to store your response for description
'$x_dsctxt',
prompt '$x_prompt' in the KDE Wallet?"; then
		kwalletcli -q -f pinentry-kwallet -e "$type-v-$key" -p "$pw"
		log "want store: yes, pw '$pw' -> $?"
	else
		# create blacklist entry for this answer
		kwalletcli -q -f pinentry-kwallet -e "$type-B-$key" -p yes
		log "want store: no"
	fi
	[[ $type = pass ]] && [[ -n $pw ]] && io_p_out -r "D $pw"
	io_p_out -r -- "$resp"
}

tried_sub=0
set -A queued_lines

ensure_coprocess() {
	(( tried_sub == 0 )) || return 0
	tried_sub=1

	# ensure the UTF-8 locale is running and LC_CTYPE is populated
	if command -v locale >/dev/null 2>&1; then
		# expand LANG/LC_*/LC_ALL to LC_*
		x=$(locale | sed -n '/^LC_/s//export &/p')
		if [[ -n $x ]]; then
			eval "$x"
			unset LANG LC_ALL
		else
			log "expanding locale failed: result={{{ $(locale 2>&1) }}}"
			if [[ -n $LC_ALL ]]; then
				export LC_CTYPE=$LC_ALL
			elif [[ -z $LC_CTYPE && -n $LANG ]]; then
				export LC_CTYPE=$LANG
			fi
		fi
		if ! y=$(locale charmap); then
			log "locale charmap command failed: result={{{ $y }}}"
			y=
		fi
		if [[ $y != @(utf|UTF|cesu|CESU)?(-)8 ]]; then
			log "no UTF-8 locale; switching to $substlocale"
			if [[ -n $x ]]; then
				export LC_CTYPE=$substlocale
			else
				export LC_CTYPE=$substlocale LC_ALL=$substlocale
				unset LANG
			fi
			[[ $(locale charmap 2>&1) = @(utf|UTF|cesu|CESU)?(-)8 ]] || \
			    log "WARNING: still no UTF-8 locale!"
		fi
	elif [[ ${LC_ALL:-${LC_CTYPE:-${LANG}}} != ?(*[!A-Za-z0-9])@(utf|UTF|cesu|CESU)?(-)8?([!A-Za-z0-9]*) ]]; then
		log "no locale(1), no UTF-8 locale; switching to $substlocale"
		export LC_CTYPE=$substlocale LC_ALL=$substlocale
		unset LANG
	else
		export LC_CTYPE=${LC_ALL:-${LC_CTYPE:-${LANG}}}
	fi
	set -U # regardless
	# pass on to subprocess
	queued_lines[${#queued_lines[*]}]="OPTION lc-ctype=$LC_CTYPE"
	if have_lc_type; then
		args[${#args[*]}]=--lc-ctype
		args[${#args[*]}]=$LC_CTYPE
	fi

	# actually start the subprocess
	PINENTRY_KWALLET=set "${PINENTRY:-pinentry}" "${args[@]}" |&
	copid=$?
	log "starting coproc $copid: PINENTRY_KWALLET=set '${PINENTRY:-pinentry}' args"
	io_s_in resp || resp=
	if [[ $resp = OK@(| *) ]]; then
		have_sub=1
		log have_sub=1
		trap "kill $copid; exit 255" HUP INT QUIT TRAP PIPE TERM
	else
		have_sub=0
		log "have_sub=0${resp:+; error response from coprocess: $resp}"
		exec 3>&p; exec 3>&-
	fi
}

io_p_out 'OK ready to listen to your demands'

while io_p_in line; do
	[[ -z $line || $line = '#'* ]] && continue
	copyline=1
	case $line {
	(SETDESC)
		x_dsctxt=
		;;
	(SETDESC\ *)
		x_dsctxt=${line#SETDESC }
		;;
	(SETPROMPT)
		x_prompt=
		;;
	(SETPROMPT\ *)
		x_prompt=${line#SETPROMPT }
		;;
	(SETTITLE@(| *))
		;;
	(SET@(OK|CANCEL|NOTOK)@(| *))
		;;
	(SETERROR)
		x_errtxt=
		;;
	(SETERROR\ *)
		x_errtxt=${line#SETERROR }
		;;
	(SETQUALITYBAR*)
		;;
	(GETPIN)
		ensure_coprocess
		getit pass
		;;
	(CONFIRM)
		ensure_coprocess
		getit bool
		;;
	(MESSAGE|CONFIRM\ --one-button)
		ensure_coprocess
		;;
	(OPTION\ ?(--)ttyname[\ =]*)
		optval=${line#OPTION*ttyname}
		optval=${optval##*( )?(=)*( )}
		GPG_TTY=$optval
		export GPG_TTY
		;;
	(OPTION\ ?(--)ttytype[\ =]*)
		optval=${line#OPTION*ttytype}
		optval=${optval##*( )?(=)*( )}
		GPG_TERM=$optval
		export GPG_TERM
		;;
	(OPTION\ ?(--)lc-ctype[\ =]*)
		copyline=0
		optval=${line#OPTION*lc-ctype}
		optval=${optval##*( )?(=)*( )}
		if (( !tried_sub )); then
			LC_CTYPE=$optval
			export LC_CTYPE
		elif [[ $LC_CTYPE = "$optval" ]]; then
			io_p_out OK
		else
			io_p_out 'ERR 536871173 do not change lc-ctype during the session'
		fi
		;;
	(OPTION\ ?(--)lc-?(c)type*)
		log "invalid lc-ctype received: $line"
		io_p_out 'ERR 536871187 unknown command'
		;;
	(OPTION\ ?(--)lc-messages[\ =]*)
		optval=${line#OPTION*lc-messages}
		optval=${optval##*( )?(=)*( )}
		LC_MESSAGES=$optval
		export LC_MESSAGES
		;;
	(OPTION\ *)
		;;
	(CONFIRM\ *)
		ensure_coprocess
		(( quiet )) || print -ru2 "W: unknown CONFIRM option: $line"
		;;
	(BYE@(| *))
		trap '' PIPE
		if (( have_sub )); then
			have_sub=0
			io_s_out BYE
			io_s_in resp
		fi
		io_p_out OK
		break
		;;
	(NOP@(| *))
		# copy quietly, in case of keepalive
		(( have_sub )) || copyline=0
		;;
	(GETINFO\ pid)
		# undocumented, but used by GnuPG v2
		io_p_out D $$
		io_p_out OK
		copyline=0
		;;
	(GETINFO\ version)
		ensure_coprocess
		# undocumented, but used by GnuPG v2
		# just copy it quietly
		#XXX maybe return "ERR 536871187 unknown command"?
		;;
	(GETINFO\ *)
		ensure_coprocess
		# undocumented, but used by GnuPG v2
		(( quiet )) || print -ru2 "W: unknown GETINFO capability: $line"
		;;
	(*)
		ensure_coprocess
		(( quiet )) || print -ru2 "W: unknown line: $line"
		;;
	}
	(( copyline )) && if (( !tried_sub )); then
		queued_lines[${#queued_lines[*]}]=$line
		io_p_out OK
	elif (( have_sub )); then
		io_s_out -r -- "$line"
		resp=
		while [[ $resp != @(OK|ERR)@(| *) ]]; do
			io_s_in resp
			io_p_out -r -- "$resp"
		done
	else
		io_p_out OK
	fi
done
trap '' PIPE
if (( have_sub )); then
	io_s_out BYE
	io_s_in resp
fi
exec 2>/dev/null	# avoid "no coprocess" warnings
exec 3>&p; exec 3>&-
exit 0
