#!/usr/bin/env bash
# Tomb manager - Password Store Extension (https://www.passwordstore.org/)
# Copyright (C) 2017-2021 Alexandre PUJOL <alexandre@pujol.io>.
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

# shellcheck disable=SC2181,SC2024

readonly TOMB="${PASSWORD_STORE_TOMB:-tomb}"
readonly TOMB_FILE="${PASSWORD_STORE_TOMB_FILE:-$HOME/.password.tomb}"
readonly TOMB_KEY="${PASSWORD_STORE_TOMB_KEY:-$HOME/.password.tomb.key}"
readonly TOMB_SIZE="${PASSWORD_STORE_TOMB_SIZE:-10}"

readonly VERSION="1.3"

#
# Common colors and functions
#
readonly green='\e[0;32m' yellow='\e[0;33m' magenta='\e[0;35m'
readonly Bred='\e[1;31m' Bgreen='\e[1;32m' Byellow='\e[1;33m'
readonly Bmagenta='\e[1;35m' Bold='\e[1m' reset='\e[0m'
_message() { [ "$QUIET" = 0 ] && printf '  %b.%b  %s\n' "$Bold" "$reset" "$*" >&2; }
_warning() { [ "$QUIET" = 0 ] && printf '  %bw%b  %b%s%b\n' "$Byellow" "$reset" "$yellow" "$*" "$reset" >&2; }
_success() { [ "$QUIET" = 0 ] && printf ' %b(*)%b %b%s%b\n' "$Bgreen" "$reset" "$green" "$*" "$reset" >&2; }
_verbose() { [ "$VERBOSE" = 0 ] || printf '  %b.%b  %bpass%b %s\n' "$Bmagenta" "$reset" "$magenta" "$reset" "$*" >&2; }
_verbose_tomb() { [ "$VERBOSE" = 0 ] || printf '  %b.%b  %s\n' "$Bmagenta" "$reset" "$*" >&2; }
_error() { printf ' %b[x]%b %bError:%b %s\n' "$Bred" "$reset" "$Bold" "$reset" "$*" >&2; }
_die() { _error "$*" && exit 1; }
_in() { [[ $1 =~ (^|[[:space:]])$2($|[[:space:]]) ]] && return 0 || return 1; }

# Check program dependencies
#
# pass tomb depends on tomb
_ensure_dependencies() {
	command -v "$TOMB" &> /dev/null || _die "Tomb is not present."
}

# Get the trust level of a GPG public key.
_get_publictrust() {
	local gpg_id="$1"
	gpg --with-colons --batch --list-keys "$gpg_id" 2> /dev/null \
		| awk 'BEGIN { FS=":" } /^pub/ { print $2; exit}'
}

# $@ is the list of all the recipient used to encrypt a tomb key
is_valid_recipients() {
	typeset -a recipients
	IFS=" " read -r -a recipients <<< "$@"
	trusted='m f u w s'

	# All the keys ID must be valid (the public keys must be present in the database)
	for gpg_id in "${recipients[@]}"; do
		trust="$(_get_publictrust "$gpg_id")"
		if [[ $? != 0 ]]; then
			_warning "${gpg_id} is not a valid key ID."
			return 1
		elif ! _in "$trusted" "$trust"; then
			_warning "The key ${gpg_id} is not trusted enough"
			return 1
		fi
	done

	# At least one private key must be present
	for gpg_id in "${recipients[@]}"; do
		gpg --with-colons --batch --list-secret-keys "$gpg_id" &> /dev/null
		if [[ $? = 0 ]]; then
			return 0
		fi
	done
	return 1
}

_tomb() {
	local ii ret
	local cmd="$1"; shift
	"$TOMB" "$cmd" "$@" "$FORCE" "$DEBUG" &> "$TMP"
	ret=$?
	while read -r ii; do
		_verbose_tomb "$ii"
	done <"$TMP"
	[[ $ret == 0 ]] || _die "Unable to $cmd the password tomb."
}

# Systemd timer to close the passwod store.
# $1: Delay before to run the pass-close service
# $2: Path in the password store to save the delay (may be empty)
# return 0 on success, 1 otherwise
_timer() {
	local ret ii _tomb_name delay="$1" path="$2"
	_tmp_create
	_tomb_name="${TOMB_FILE##*/}"
	_tomb_name="${_tomb_name%.tomb}"
	sudo systemd-run --system --on-active="$delay" \
	                 --description="pass-close timer" \
	                 --unit="pass-close@$_tomb_name.service" \
	                 &> "$TMP"
	ret=$?
	while read -r ii; do
		_verbose "$ii"
	done <"$TMP"
	if [[ $ret == 0 ]]; then
		echo "$delay" > "$PREFIX/$path/.timer"
		_verbose "Timer successfully created"
		echo 0
	else
		_warning "Unable to set the timer to close the password tomb in $delay."
		echo 1
	fi
	return $ret
}

# Provide a random filename in shared memory
_tmp_create() {
	local tfile
	tmpdir	# Defines $SECURE_TMPDIR
	tfile="$(mktemp -u "$SECURE_TMPDIR/XXXXXXXXXXXXXXXXXXXX")" # Temporary file

	umask 066
	[[ $? == 0 ]] || _die "Fatal error setting permission umask for temporary files."
	[[ -r "$tfile" ]] && _die "Someone is messing up with us trying to hijack temporary files."

	touch "$tfile"
	[[ $? == 0 ]] || _die "Fatal error creating temporary file: $tfile."

	TMP="$tfile"
	return 0
}

# Set ownership when mounting a tomb
# $1: Tomb path
_set_ownership() {
	local _uid _gid path="$1"
	_uid="$(id -u "$USER")"
	_gid="$(id -g "$USER")"
	_verbose "Setting user permissions on $path"
	sudo chown -R "$_uid:$_gid" "$path" || _die "Unable to set ownership permission on $path."
}

cmd_tomb_version() {
	cat <<-_EOF
	$PROGRAM tomb $VERSION - A pass extension that helps to keep the whole tree of
	                password encrypted inside a tomb.
	_EOF
}

cmd_tomb_usage() {
	cmd_tomb_version
	echo
	cat <<-_EOF
	Usage:
	    $PROGRAM tomb [-n] [-t time] [-f] [-p subfolder] gpg-id...
	        Create and initialise a new password tomb
	        Use gpg-id for encryption of both tomb and passwords

	    $PROGRAM open [subfolder] [-t time] [-f]
	        Open a password tomb

	    $PROGRAM close [store]
	        Close a password tomb

	    $PROGRAM timer [store]
	        Show timer status

	Options:
	    -n, --no-init  Do not initialise the password store
	    -t, --timer    Close the store after a given time
	    -p, --path     Create the store for that specific subfolder
	    -f, --force    Force operation (i.e. even if swap is active)
	    -q, --quiet    Be quiet
	    -v, --verbose  Be verbose
	    -d, --debug    Print tomb debug messages
	        --unsafe   Speed up tomb creation (for testing only)
	    -V, --version  Show version information.
	    -h, --help     Print this help message and exit.

	More information may be found in the pass-tomb(1) man page.
	_EOF
}

# Open a password tomb
cmd_open() {
	local path="$1"; shift;

	# Sanity checks
	check_sneaky_paths "$path" "$TOMB_FILE" "$TOMB_KEY"
	[[ -e "$TOMB_FILE" ]] || _die "There is no password tomb to open."
	[[ -e "$TOMB_KEY" ]] || _die "There is no password tomb key."

	# Open the passwod tomb
	_tmp_create
	_verbose "Opening the password tomb $TOMB_FILE using the key $TOMB_KEY"
	_tomb open "$TOMB_FILE" -k "$TOMB_KEY" -g "$PREFIX/$path"
	_set_ownership "$PREFIX/$path"

	# Read, initialise and start the timer
	local timed=1
	if [[ -z "$TIMER" ]]; then
		if [[ -e "$PREFIX/$path/.timer" ]]; then
			TIMER="$(cat "$PREFIX/$path/.timer")"
			[[ -z "$TIMER" ]] || timed="$(_timer "$TIMER" "$path")"
		fi
	else
		timed="$(_timer "$TIMER" "$path")"
	fi

	# Success!
	_success "Your password tomb has been opened in $PREFIX/."
	_message "You can now use pass as usual."
	if [[ $timed == 0 ]]; then
		_message "This password store will be closed in $TIMER"
	else
		_message "When finished, close the password tomb using 'pass close'."
	fi
	return 0
}

# Close a password tomb
cmd_close() {
	local _tomb_name _tomb_file="$1"
	[[ -z "$_tomb_file" ]] && _tomb_file="$TOMB_FILE"

	# Sanity checks
	check_sneaky_paths "$_tomb_file"
	_tomb_name="${_tomb_file##*/}"
	_tomb_name="${_tomb_name%.tomb}"
	[[ -z "$_tomb_name" ]] && _die "There is no password tomb."

	_tmp_create
	_verbose "Closing the password tomb $_tomb_file"
	_tomb close "$_tomb_name"

	_success "Your password tomb has been closed."
	_message "Your passwords remain present in $_tomb_file."
	return 0
}

# Show timer status
cmd_timer() {
	local _tomb_name _tomb_file="$1"
	[[ -z "$_tomb_file" ]] && _tomb_file="$TOMB_FILE"

	# Sanity checks
	check_sneaky_paths "$_tomb_file"
	_tomb_name="${_tomb_file##*/}"
	_tomb_name="${_tomb_name%.tomb}"
	[[ -z "$_tomb_name" ]] && _die "There is no password tomb."

	if systemctl is-active "pass-close@$_tomb_name.timer" &> /dev/null; then
		systemctl status "pass-close@$_tomb_name.timer"
	else
		_warning "There is no active timer for $_tomb_file."
		sudo systemctl status "pass-close@$_tomb_name.service"
	fi

	return 0
}

# Create a new password tomb and initialise the password repository.
# $1: path subfolder
# $@: gpg-ids
cmd_tomb() {
	local path="$1"; shift;
	typeset -a RECIPIENTS
	[[ -z "$*" ]] && _die "$PROGRAM $COMMAND [-n] [-t time] [-p subfolder] gpg-id..."
	IFS=" " read -r -a RECIPIENTS <<< "$@"

	# Sanity checks
	check_sneaky_paths "$path" "$TOMB_FILE" "$TOMB_KEY"
	if ! is_valid_recipients "${RECIPIENTS[@]}"; then
		_die "You set an invalid GPG ID."
	elif [[ -e "$TOMB_KEY" ]]; then
		_die "The tomb key $TOMB_KEY already exists. I won't overwrite it."
	elif [[ -e "$TOMB_FILE" ]]; then
		_die "The password tomb $TOMB_FILE already exists. I won't overwrite it."
	elif [[ "$TOMB_SIZE" -lt 10 ]]; then
		_die "A password tomb cannot be smaller than 10 MB."
	fi
	if [[ $UNSAFE -ne 0 ]]; then
		_warning "Using unsafe mode to speed up tomb generation."
		_warning "Only use it for testing purposes."
		local unsafe=(--unsafe)
	fi

	# Sharing support
	local recipients_arg tmp_arg
	if [ "${#RECIPIENTS[@]}" -gt 1 ]; then
		tmp_arg="${RECIPIENTS[*]}"
		recipients_arg=${tmp_arg// /,}
	else
		recipients_arg="${RECIPIENTS[0]}"
	fi

	# Create the password tomb
	_tmp_create
	_verbose "Creating a password tomb with the GPG key(s): ${RECIPIENTS[*]}"
	_tomb dig "$TOMB_FILE" -s "$TOMB_SIZE"
	_tomb forge "$TOMB_KEY" -gr "$recipients_arg" "${unsafe[@]}"
	_tomb lock "$TOMB_FILE" -k "$TOMB_KEY" -gr "$recipients_arg"
	_tomb open "$TOMB_FILE" -k "$TOMB_KEY" -gr "$recipients_arg" "$PREFIX/$path"
	_set_ownership "$PREFIX/$path"

	# Use the same recipients to initialise the password store
	local ret path_cmd=()
	if [[ $NOINIT -eq 0 ]]; then
		[[ -z "$path" ]] || path_cmd=("--path=${path}")
		ret="$(cmd_init "${RECIPIENTS[@]}" "${path_cmd[@]}")"
		if [[ ! -e "$PREFIX/$path/.gpg-id" ]]; then
			_warning "$ret"
			_die "Unable to initialise the password store."
		fi
	fi

	# Initialise the timer
	local timed=1
	[[ -z "$TIMER" ]] || timed="$(_timer "$TIMER" "$path")"

	# Success!
	_success "Your password tomb has been created and opened in $PREFIX."
	[[ -z "$ret" ]] || _success "$ret"
	_message "Your tomb is: $TOMB_FILE"
	_message "Your tomb key is: $TOMB_KEY"
	if [[ -z "$ret" ]]; then
		_message "You need to initialise the store with 'pass init gpg-id...'."
	else
		_message "You can now use pass as usual."
	fi
	if [[ $timed == 0 ]]; then
		_message "This password store will be closed in $TIMER"
	else
		_message "When finished, close the password tomb using 'pass close'."
	fi
	return 0
}

# Check dependencies are present or bail out
_ensure_dependencies

# Global options
UNSAFE=0
VERBOSE=0
QUIET=0
FORCE=""
DEBUG=""
NOINIT=0
TIMER=""

# Getopt options
small_arg="vdhVp:qnt:f"
long_arg="verbose,debug,help,version,path:,unsafe,quiet,no-init,timer:,force"
opts="$($GETOPT -o $small_arg -l $long_arg -n "$PROGRAM $COMMAND" -- "$@")"
err=$?
eval set -- "$opts"
while true; do case $1 in
	-q|--quiet) QUIET=1; VERBOSE=0; DEBUG=""; shift ;;
	-v|--verbose) VERBOSE=1; shift ;;
	-d|--debug) DEBUG="-D"; VERBOSE=1; shift ;;
	-f|--force) FORCE="--force"; shift ;;
	-h|--help) shift; cmd_tomb_usage; exit 0 ;;
	-V|--version) shift; cmd_tomb_version; exit 0 ;;
	-p|--path) id_path="$2"; shift 2 ;;
	-t|--timer) TIMER="$2"; shift 2 ;;
	-n|--no-init) NOINIT=1; shift ;;
	--unsafe) UNSAFE=1; shift ;;
	--) shift; break ;;
esac done

[[ -z "$TIMER" ]] || command -v systemd-run &> /dev/null || _die "systemd-run is not present."
[[ $err -ne 0 ]] && cmd_tomb_usage && exit 1
[[ "$COMMAND" == "tomb" ]] && cmd_tomb "$id_path" "$@"
