#!/usr/bin/env bash
# sshkeyupdate -- download ~/.ssh/authorized_keys from central URL

readonly unix_user="grawity"
readonly src_url="http://nullroute.lt/~grawity/files/authorized_keys.txt"
readonly signer_key="$HOME/bin/.pubkey.pgp"
readonly signer_fpr="2357E10CEF4F7ED27E233AD5D24F6CB2C1B52632"

PATH="$HOME/bin:$PATH"

. lib.bash || exit
. ~/bin/lib/libfilterfile.bash || exit
. ~/bin/lib/libhttpfetch.bash || exit

usage() {
	echo "Usage: $progname [-f] [-q] [-i]"
	echo ""
	echo_opt "-f"		"proceed on username mismatch"
	echo_opt "-q"		"quietly exit if update is not to be done"
	echo_opt "-i"		"import embedded signing key"
}

filter_keys() {
	filter_file 'match_hostname'
}

fetch() {
	local url=$1 out=$2
	local maxtries=3 delay=3 attempt=1
	while true; do
		debug "fetching $url (try #$attempt)"
		if http_fetch "$url" "$out"; then
			return 0
		elif (( $? == 99 )); then
			break
		fi
		rm -f "$out"
		if (( attempt++ >= maxtries )); then
			break
		fi
		sleep $delay
		(( delay *= 2 ))
	done
	return 1
}

verify_gpg() {
	local input=$1 output=$2 status=

	export GNUPGHOME="$workdir/gnupg"
	mkdir -m 0700 "$GNUPGHOME" || return

	if ! status=$(gpg \
			--batch \
			--quiet \
			--no-options \
			--no-default-keyring \
			--no-auto-key-import \
			--no-auto-key-retrieve \
			--keyring="$signer_key" \
			--trust-model=always \
			--status-fd=1 \
			--output="$output" \
			--decrypt \
			"$input" 2>/dev/null)
	then
		vmsg "signature verification failed" >&2
		echo "$status" >&2
		return 1
	elif ! grep -Eqs "^\\[GNUPG:\\] VALIDSIG $signer_fpr " <<< "$status"; then
		vmsg "signer mismatch" >&2
		echo "$status" >&2
		return 1
	else
		return 0
	fi
}

verify_gpgv() {
	local input=$1 output=$2 status=

	export GNUPGHOME="$workdir/gnupg"
	mkdir -m 0700 "$GNUPGHOME" || return

	if ! status=$(gpgv \
			--status-fd=1 \
			--keyring="$signer_key" \
			--output="$output" \
			"$input" 2>/dev/null); then
		vmsg "signature verification failed" >&2
		echo "$status" >&2
		return 1
	elif ! grep -Eqs "^\\[GNUPG:\\] VALIDSIG $signer_fpr " <<< "$status"; then
		vmsg "signer mismatch" >&2
		echo "$status" >&2
		return 1
	else
		return 0
	fi
}

verify_sq() {
	local input=$1 output=$2 status=

	export SEQUOIA_HOME="$workdir/sq"
	mkdir -m 0700 "$SEQUOIA_HOME" &&
	sq cert import "$signer_key" >/dev/null || return 2

	if ! status=$(sq verify \
			--cleartext \
			--signer="$signer_fpr" \
			--signer-file="$signer_key" \
			--output="$output" \
			"$input" 2>&1); then
		vmsg "validation failed" >&2
		echo "$status" >&2
		return 1
	fi

	return 0
}

verify_sop() {
	local input=$1 output=$2

	${SOP?} inline-verify "$signer_key" < "$input" > "$output"
}

verify_sqv() {
	local input=$1 output=$2

	sqv --cleartext --keyring="$signer_key" --output="$output" "$input" > /dev/null
}

verify() {
	if [ "$SOP" = gpgv ]; then
		verify_gpgv "$@"
	elif [ "$SOP" = gpg ]; then
		verify_gpg "$@"
	elif [ "$SOP" = sqv ]; then
		verify_sqv "$@"
	elif [ "$SOP" = sq ]; then
		verify_sq "$@"
	elif [ "$SOP" ]; then
		verify_sop "$@"
	elif have sqv; then
		verify_sqv "$@"
	elif have sqop; then
		SOP=sqop verify_sop "$@"
	elif have rsop; then
		SOP=rsop verify_sop "$@"
	elif have gosop; then
		SOP=gosop verify_sop "$@"
	elif have gpgv; then
		verify_gpgv "$@"
	elif have gpg; then
		verify_gpg "$@"
	elif have sq; then
		verify_sq "$@"
	else
		vmsg "gpg or sop is not installed" >&2
		return 2
	fi
}

umask 077

opt_testfilter=0
opt_force=0
opt_importkey=0
opt_quiet=0
dst_file=~/.ssh/authorized_keys

while getopts :Ffiq OPT; do
	case $OPT in
	F) opt_testfilter=1;;
	f) opt_force=1;;
	i) opt_importkey=1;;
	q) opt_quiet=1;;
	*) lib:die_getopts;;
	esac
done; shift $[OPTIND-1]

if (( $# )); then
	vdie "excess arguments"
fi

if (( opt_testfilter )); then
	if [[ -t 0 ]]; then
		vdie "filter test mode requires file input"
	fi
	if [[ -t 1 ]]; then
		export FILTERDEBUG=1
		exec >/dev/null
	fi
	filter_keys
	exit
fi

if [[ -e "$dst_file.noupdate" ]] || grep -qs "^# *NOUPDATE" "$dst_file"; then
	if (( opt_force )); then
		true
	elif (( !opt_quiet )); then
		vdie "\"no update\" marker found, exiting"
	else
		exit 0
	fi
fi

if [[ "$LOGNAME" != "$unix_user" ]]; then
	if [[ -e "$dst_file.optin" ]]; then
		true
	elif (( opt_force )); then
		vmsg "username mismatch, creating opt-in marker"
		echo "$LOGNAME on $(date)" > "$dst_file.optin"
	elif (( !opt_quiet )); then
		vdie "username mismatch, this program isn't for you"
	else
		exit 0
	fi
fi

# ensure key exists in keyring

if (( opt_importkey )); then
	vmsg "importing embedded owner key into GnuPG keyring"
	gpg --import "$signer_key"
	echo "$signer_fpr:6:" | gpg --import-ownertrust
fi

# download new pubkeys

workdir=$(mktemp -d /tmp/sshkeyupdate.XXXXXX) || exit
tmp_signed="$workdir/signed.txt"
tmp_plain="$workdir/output.txt"

mkdir -p -m 0700 "${dst_file%/*}"

if ! fetch "$src_url" "$tmp_signed"; then
	rm -f "$tmp_signed"
	vdie "fetch of $src_url failed"
elif ! test -s "$tmp_signed"; then
	rm -f "$tmp_signed"
	vdie "empty file fetched from $src_url"
fi

if ! verify "$tmp_signed" "$tmp_plain"; then
	if (( $? == 2 )); then
		rm -f "$tmp_plain"
		vdie "verification of $tmp_signed could not be performed"
	else
		rm -f "$tmp_plain"
		vdie "verification of $tmp_signed failed"
	fi
fi

local_file="$dst_file.local"

{
	echo "# updated: at $(date +"%Y-%m-%d %H:%M:%S %z")"
	echo "# updated: from $src_url"
	cat "$tmp_plain" | tr -d '\r'
	if [[ -s $local_file ]]; then
		echo "# updated: from $local_file"
		cat "$local_file"
	fi
	echo "# updated: end"
} | filter_keys > "$dst_file"

rm -rf "${workdir?}"

if [[ -t 1 ]]; then
	num=$(grep -c '^[^#]' "$dst_file")
	vmsg "imported $num keys"
fi
