#!/bin/sh
#
# Copyright (c) 2026, Jesús Daniel Colmenares Oviedo <DtxdF@disroot.org>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

lib_load "${LIBDIR}/mount"

secrets_desc="Secrets management for jails."

SECRETS_IS_UPDATE=false

secrets_main()
{
	local entity="$1"; shift
	if lib_check_empty "${entity}"; then
		secrets_usage
		exit ${EX_USAGE}
	fi

	case "${entity}" in
		cat|create|init|ls|rm|rmdir|update|use) ;;
		*) secrets_usage; exit ${EX_USAGE} ;;
	esac

	init || exit $?

	if [ "${entity}" = "init" ]; then
		return 0
	fi

	secrets_${entity} "$@"
}

secrets_cat()
{
	local name
	name="$1"

	if lib_check_empty "${name}"; then
		secrets_usage
		exit ${EX_USAGE}
	fi

	if ! lib_check_secret "${name}"; then
		lib_err ${EX_DATAERR} "Invalid secret name '${name}'"
	fi

	local group
	group=`lib_jailparam_name "${name}" /`

	name=`lib_jailparam_value "${name}" /`

	local backend
	backend=`_get_backend` || exit $?

	local groupsdir
	groupsdir="${SECRETSDIR}/groups/${backend}"

	local groupdir
	groupdir="${groupsdir}/${group}"

	local secretfile
	secretfile="${groupdir}/${name}"

	if [ ! -s "${secretfile}" ]; then
		lib_err ${EX_CANTCREAT} "Secret '${group}/${name}' does not exist."
	fi

	_decrypt "${secretfile}"
}

_decrypt()
{
	local backend
	backend=`_get_backend` || exit $?

	local backendsdir="${SECRETS_BACKENDSDIR}"
	local backenddir="${backendsdir}/${backend}"
	local keyfile="${backenddir}/key.txt"

	"${SECRETS_BACKEND}" -d -i "${keyfile}" "$1"
}

secrets_create()
{
	local _o
	local from_secret=false
	local from_string=false
	local mode=
	local uid=
	local gid=
	local append_newline=true

	while getopts ":NSsg:m:u:" _o; do
		case "${_o}" in
			g|m|u)
				if lib_check_empty "${OPTARG}"; then
					secrets_usage
					exit ${EX_USAGE}
				fi
				;;
		esac

		case "${_o}" in
			N)
				append_newline=false
				;;
			S)
				from_secret=true
				;;
			s)
				from_string=true
				;;
			g)
				gid="${OPTARG}"
				;;
			m)
				mode="${OPTARG}"
				;;
			u)
				uid="${OPTARG}"
				;;
			*)
				secrets_usage
				exit ${EX_USAGE}
				;;
		esac
	done
	shift $((OPTIND-1))

	if ${from_secret} && ${from_string}; then
		secrets_usage
		exit ${EX_USAGE}
	fi

	local name
	name="$1"

	if lib_check_empty "${name}"; then
		secrets_usage
		exit ${EX_USAGE}
	fi

	if ! lib_check_secret "${name}"; then
		lib_err ${EX_DATAERR} "Invalid secret name '${name}'"
	fi

	local group
	group=`lib_jailparam_name "${name}" /`

	name=`lib_jailparam_value "${name}" /`

	local backend
	backend=`_get_backend` || exit $?

	local groupsdir
	groupsdir="${SECRETSDIR}/groups/${backend}"

	if [ ! -d "${groupsdir}" ]; then
		mkdir -p -- "${groupsdir}" || exit $?
	fi

	local groupdir
	groupdir="${groupsdir}/${group}"

	local secretfile
	secretfile="${groupdir}/${name}"

	if ! ${SECRETS_IS_UPDATE}; then
		if [ -s "${secretfile}" ]; then
			lib_err ${EX_CANTCREAT} "Secret '${group}/${name}' already exists."
		fi
	else
		if [ ! -s "${secretfile}" ]; then
			lib_err ${EX_CANTCREAT} "Secret '${group}/${name}' does not exist."
		fi
	fi

	local value
	value="$2"

	if [ ! -d "${groupdir}" ]; then
		# Allow access to everyone but restricted per-file mode.
		install -d -m 0755 "${groupdir}" || exit $?
	fi

	if ${SECRETS_IS_UPDATE}; then
		if [ -z "${mode}" ]; then
			mode=`stat -f %Lp -- "${secretfile}"` || exit $?
		fi

		if [ -z "${uid}" ]; then
			uid=`stat -f %u -- "${secretfile}"` || exit $?
		fi

		if [ -z "${gid}" ]; then
			gid=`stat -f %g -- "${secretfile}"` || exit $?
		fi

		chmod -- "${mode}" "${secretfile}" || exit $?
		chown -- "${uid}:${gid}" "${secretfile}" || exit $?
	else
		mode="${mode:-0400}"
		uid="${uid:-0}"
		gid="${gid:-0}"

		install -m "${mode}" -o "${uid}" -g "${gid}" /dev/null "${secretfile}" || exit $?
	fi

	if "${from_secret}" || "${from_string}"; then
		if lib_check_empty "${value}"; then
			secrets_usage
			exit ${EX_USAGE}
		fi

		if "${from_secret}"; then
			secrets_cat "${value}" | _encrypt "${secretfile}" || exit $?
		else
			local newline=

			if ${append_newline}; then
				newline="\n"
			fi

			printf "%s${newline}" "${value}" | _encrypt "${secretfile}" || exit $?
		fi
	else
		_encrypt "${secretfile}" "${value}" || exit $?
	fi

	if ! ${SECRETS_IS_UPDATE}; then
		return 0
	fi

	local ramdisk
	ramdisk="${SECRETSDIR}/ramdisk"

	local ramdisk_groupdir
	ramdisk_groupdir="${ramdisk}/${group}"

	local ramdisk_secretfile
	ramdisk_secretfile="${ramdisk_groupdir}/${name}"

	if [ ! -f "${ramdisk_secretfile}" ]; then
		return 0
	fi

	chmod -- "${mode}" "${ramdisk_secretfile}" || exit $?
	chown -- "${uid}:${gid}" "${ramdisk_secretfile}" || exit $?

	secrets_cat "${group}/${name}" > "${ramdisk_secretfile}" || exit $?
}

_encrypt()
{
	local backend
	backend=`_get_backend` || exit $?

	local backendsdir="${SECRETS_BACKENDSDIR}"
	local backenddir="${backendsdir}/${backend}"
	local keyfile="${backenddir}/key.txt"
	
	local recipient
	recipient=`"${SECRETS_BACKEND}-keygen" -y -- "${keyfile}"` || exit $?

	if [ -n "$2" ]; then
		"${SECRETS_BACKEND}" -e -r "${recipient}" -o "$1" -- "$2"
	else
		"${SECRETS_BACKEND}" -e -r "${recipient}" -o "$1"
	fi
}

init()
{
	local backend
	backend=`_get_backend` || exit $?

	if ! which -s "${SECRETS_BACKEND}"; then
		lib_err ${EX_UNAVAILABLE} "${SECRETS_BACKEND} is not installed. Cannot continue ..."
	fi

	# We can do this because we've only implemented rage/age, which works the same
	# way and has a compatible interface.

	(
		lockf -s 9 || return 0

		local backendsdir="${SECRETS_BACKENDSDIR}"
		local backenddir="${backendsdir}/${backend}"
		local keyfile="${backenddir}/key.txt"

		if ! lib_zfs_mkdir "${SECRETSDIR}" "${ZFS_SECRETS_NAME}"; then
			lib_err ${EX_IOERR} "Error creating ${SECRETSDIR}"
		fi

		if ! chmod 0700 "${SECRETSDIR}"; then
			lib_err ${EX_SOFTWARE} "Error restricing permissions in ${SECRETSDIR}"
		fi

		if [ ! -d "${backendsdir}" ]; then
			mkdir -p -- "${backendsdir}" || exit $?
		fi

		if [ ! -d "${backenddir}" ]; then
			install -d -m 0700 -- "${backenddir}" || exit $?
		fi

		if [ -f "${keyfile}" ]; then
			return 0
		fi

		local output errlevel

		output=`"${SECRETS_BACKEND}-keygen" -o "${keyfile}" 2>&1`

		errlevel=$?

		if [ ${errlevel} -ne 0 ]; then
			lib_err ${errlevel} -- "${output}"
		fi
	) 9> "${TMPDIR}/secrets-init.lock"
}

_get_backend()
{
	local backend="${SECRETS_BACKEND}"

	case "${backend}" in
		age|rage) backend="age" ;;
		*) lib_err ${EX_CONFIG} "Unknown secrets backend: ${backend}" ;;
	esac

	echo "${backend}"
}

secrets_ls()
{
	local _o
	opt_ramdisk=false

	while getopts ":R" _o; do
		case "${_o}" in
			R)
				opt_ramdisk=true
				;;
			*)
				secrets_usage
				exit ${EX_USAGE}
				;;
		esac
	done
	shift $((OPTIND-1))

	local backend
	backend=`_get_backend` || exit $?

	local groupsdir

	if ${opt_ramdisk}; then
		groupsdir="${SECRETSDIR}/ramdisk"
	else
		groupsdir="${SECRETSDIR}/groups/${backend}"
	fi

	if [ ! -d "${groupsdir}" ]; then
		return 0
	fi

	local name="$1"

	if lib_check_empty "${name}"; then
		ls -1A -- "${groupsdir}" | while IFS= read -r group; do
			if lib_check_emptydir "${groupsdir}/${group}"; then
				continue
			fi

			printf "%s\n" "${group}"
		done
		return $?
	fi

	local valid=true

	if lib_check_ispath "${name}"; then
		if ! lib_check_secret "${name}"; then
			valid=false
		fi
	else
		if ! lib_check_secretname "${name}"; then
			valid=false
		fi
	fi

	if ! ${valid}; then
		lib_err ${EX_DATAERR} "Invalid secret name '${name}'"
	fi

	local object="${groupsdir}/${name}"

	if [ ! -d "${object}" ] && [ ! -f "${object}" ]; then
		lib_err ${EX_NOINPUT} -- "${name}: No such group or secret."
	fi

	if [ -f "${object}" ]; then
		printf "%s\n" "${name}"
	else
		if lib_check_emptydir "${object}"; then
			lib_err ${EX_NOINPUT} -- "${name}: No such group or secret."
		fi

		ls -1A -- "${object}" || exit $?
	fi
}

secrets_rm()
{
	local name
	name="$1"

	if lib_check_empty "${name}"; then
		secrets_usage
		exit ${EX_USAGE}
	fi

	if ! lib_check_secret "${name}"; then
		lib_err ${EX_DATAERR} "Invalid secret name '${name}'"
	fi

	local group
	group=`lib_jailparam_name "${name}" /`

	name=`lib_jailparam_value "${name}" /`

	local backend
	backend=`_get_backend` || exit $?

	local groupsdir
	groupsdir="${SECRETSDIR}/groups/${backend}"

	local groupdir
	groupdir="${groupsdir}/${group}"

	local secretfile
	secretfile="${groupdir}/${name}"

	if [ ! -s "${secretfile}" ]; then
		lib_err ${EX_CANTCREAT} "Secret '${group}/${name}' does not exist."
	fi

	local ramdisk
	ramdisk="${SECRETSDIR}/ramdisk"

	local ramdisk_groupdir
	ramdisk_groupdir="${ramdisk}/${group}"

	local ramdisk_secretfile
	ramdisk_secretfile="${ramdisk_groupdir}/${name}"

	if [ -f "${ramdisk_secretfile}" ]; then
		rm -f -- "${ramdisk_secretfile}"
	fi

	rm -f -- "${secretfile}"
}

secrets_rmdir()
{
	local group
	group="$1"

	if lib_check_empty "${group}"; then
		secrets_usage
		exit ${EX_USAGE}
	fi

	if ! lib_check_secretname "${group}"; then
		lib_err ${EX_DATAERR} "Invalid group name '${group}'"
	fi

	local backend
	backend=`_get_backend` || exit $?

	local groupsdir
	groupsdir="${SECRETSDIR}/groups/${backend}"

	local groupdir
	groupdir="${groupsdir}/${group}"

	if [ ! -d "${groupdir}" ] || lib_check_emptydir "${groupdir}"; then
		lib_err ${EX_CANTCREAT} "Group secret '${group}' does not exist."
	fi

	local ramdisk
	ramdisk="${SECRETSDIR}/ramdisk"

	local ramdisk_groupdir
	ramdisk_groupdir="${ramdisk}/${group}"

	if [ -d "${ramdisk_groupdir}" ]; then
		rm -f -- "${ramdisk_groupdir}"/*
	fi

	# This is simply an optimization to avoid checking for mounted paths. ls command
	# will consider a group to be nonexistent if it is empty.
	rm -f -- "${groupdir}"/*
}

secrets_update()
{
	SECRETS_IS_UPDATE=true

	secrets_create "$@"
}

secrets_use()
{
	local group
	group="$1"

	if lib_check_empty "${group}"; then
		secrets_usage
		exit ${EX_USAGE}
	fi

	if ! lib_check_secretname "${group}"; then
		lib_err ${EX_DATAERR} "Invalid group name '${group}'"
	fi

	local backend
	backend=`_get_backend` || exit $?

	local groupsdir
	groupsdir="${SECRETSDIR}/groups/${backend}"

	local groupdir
	groupdir="${groupsdir}/${group}"

	if [ ! -d "${groupdir}" ] || lib_check_emptydir "${groupdir}"; then
		lib_err ${EX_CANTCREAT} "Group secret '${group}' does not exist."
	fi

	local errlevel

	local ramdisk
	ramdisk="${SECRETSDIR}/ramdisk"

	local ramdisk_groupdir
	ramdisk_groupdir="${ramdisk}/${group}"

	(
		lockf -s 9

		if [ ! -d "${ramdisk}" ]; then
			mkdir -p -- "${ramdisk}" || exit $?
		fi

		local tflag=
		if [ "${ENABLE_ZFS}" != 0 ]; then
			tflag="-t nozfs"
		fi

		local mounted=`lib_mountpoint_mounted ${tflag} -s "${ramdisk}"`
		if [ -z "${mounted}" ]; then
			local mdmfs_flags=

			if [ ${ENABLE_DEBUG} != "0" ]; then
				mdmfs_flags="-LX"
			fi

			mdmfs ${mdmfs_flags} -SMnt -s "${SECRETS_MDMFS_SIZE}" md "${ramdisk}" >&2 || exit $?
		fi
	) 9> "${TMPDIR}/secrets-use.lock"

	errlevel=$?

	if [ ${errlevel} -ne 0 ]; then
		exit ${errlevel}
	fi

	(
		lockf -s 9 || return 0

		if [ ! -d "${ramdisk_groupdir}" ]; then
			install -d -m 0755 "${ramdisk_groupdir}" || exit $?
		fi

		local secrets
		secrets=`secrets_ls "${group}"` || exit $?

		local secret
		for secret in ${secrets}; do
			local ramdisk_file
			ramdisk_file="${ramdisk_groupdir}/${secret}"

			if [ -s "${ramdisk_file}" ]; then
				continue
			fi

			local secretfile
			secretfile="${groupdir}/${secret}"

			if [ ! -s "${secretfile}" ]; then
				continue
			fi

			local uid
			uid=`stat -f %u -- "${secretfile}"` || exit $?

			local gid
			gid=`stat -f %g -- "${secretfile}"` || exit $?

			local mode
			mode=`stat -f %Lp -- "${secretfile}"` || exit $?

			install -m "${mode}" -o "${uid}" -g "${gid}" /dev/null "${ramdisk_file}" || exit $?

			secrets_cat "${group}/${secret}" > "${ramdisk_file}" || exit $?
		done
	) 9> "${TMPDIR}/secrets-use.${group}.lock"
}

secrets_help()
{
	man 1 appjail-secrets
}

secrets_usage()
{
	cat << EOF
usage: secrets cat <group>/<secret>
       secrets [create|update] [-g <gid>] [-m <mode>] [-u <uid>] <group>/<secret>
               [<file>]
       secrets [create|update] [-N] [-g <gid>] [-m <mode>] [-u <uid>] -s
               <group>/<secret> <string>
       secrets [create|update] [-g <gid>] [-m <mode>] [-u <uid>] -S
               <group>/<secret> <group>/<secret>
       secrets init
       secrets ls [-R] [<group>[/<secret>]]
       secrets rm <group>/<secret>
       secrets rmdir <group>
       secrets use <group>
EOF
}
