#!/bin/sh
# Puts ForestVPN's software source and the key that signs it on the router (PRD INS-1, WP A-7). The one place that knows where a
# key and a source line go on opkg (24.10 and older) and on apk (25.12), so a later update from our repository is the package
# manager's own, signature checked.
#
#   feed-place <dir>             <dir> holds `base` (the repository's https address) and the key of the router's package manager:
#                                `ipk.pub` (usign) for opkg, `apk.pem` (EC P-256) for apk. Writes the key and, if the router has no
#                                ForestVPN source yet, appends one line to the custom source list. Existing lines are never touched.
#   feed-place --remove [<dir>]  takes our line and our key away again (the package's removal, INS-5; <dir> defaults to the one the
#                                package carried). Other sources and keys stay, whatever they look like.
#
# Used by: the package's post-install (<dir> = /usr/share/forestvpn/feed, written by the build), install.sh, the setup file's
# bootstrap and the offline bundle (each carries a copy of this file: packaging/openwrt/setup/build.py puts it in).
# A key the signed release named as revoked (the updater records its id in /etc/forestvpn/revoked_keys, review-router M2) is not placed
# again, whichever install form asks. On opkg the fingerprint of every key placed is recorded in /etc/forestvpn/placed_keys: the updater
# removes a revoked key only if it is on that list (SEC-1 TM-002: OpenWrt's own keys are never on it).
# Never: fetches anything, calls the package manager (post-install runs inside its lock), writes a source that is not https,
# touches wan or lan, or lowers the package manager's trust.
#
# Test seam: FVPN_FEED_ETC (the /etc to use, default /etc), FVPN_FEED_FMT (opkg or apk; default: apk when the apk tool is here).
ETC=${FVPN_FEED_ETC:-/etc}
FMT=${FVPN_FEED_FMT:-}
[ -n "$FMT" ] || { FMT=opkg; command -v apk >/dev/null 2>&1 && FMT=apk; }
die() { echo "forestvpn: $*" >&2; exit 1; }
# two records: the router's own (the updater writes it, it survives an uninstall) and the one this package or setup file carries (the ids
# the release that built it had revoked, SEC-1 TM-003: a reinstall from an older file still refuses a key revoked since)
revoked() { grep -qxsF "$1" "$ETC/forestvpn/revoked_keys" "$DIR/revoked_keys"; }

arch() { sed -n "s/^DISTRIB_ARCH=//p" "$ETC/openwrt_release" 2>/dev/null | tr -d "'\"" | head -n 1; }
# a whole line of a file ends in a newline before another is appended
append() { # <file> <line>
	mkdir -p "$(dirname "$1")" || return 1
	[ -s "$1" ] && [ -n "$(tail -c 1 "$1")" ] && echo >> "$1"
	echo "$2" >> "$1"
}

# a router whose settings say the key and line were ours: the line this directory describes, the key with these exact bytes
remove() { # <dir>
	_base=$(head -n 1 "$1/base" 2>/dev/null | tr -d '\r'); _arch=$(arch)
	if [ "$FMT" = apk ]; then
		_f=$ETC/apk/repositories.d/customfeeds.list
		[ -f "$_f" ] && [ -n "$_base" ] && { grep -vxF "$_base/apk/$_arch/packages.adb" "$_f" > "$_f.new"; cat "$_f.new" > "$_f"; rm -f "$_f.new"; }
		for _k in "$ETC"/apk/keys/forestvpn-*; do [ -f "$_k" ] && cmp -s "$_k" "$1/apk.pem" && rm -f "$_k"; done
	else
		_f=$ETC/opkg/customfeeds.conf
		[ -f "$_f" ] && { grep -Ev '^src/gz[ 	]+forestvpn[ 	]' "$_f" > "$_f.new"; cat "$_f.new" > "$_f"; rm -f "$_f.new"; }
		[ -f "$1/ipk.pub" ] && command -v usign >/dev/null 2>&1 && _p=$(usign -F -p "$1/ipk.pub" 2>/dev/null) && [ -n "$_p" ] && rm -f "$ETC/opkg/keys/$_p"
	fi
	return 0
}
if [ "$1" = --remove ]; then remove "${2:-/usr/share/forestvpn/feed}"; exit 0; fi

DIR=$1
[ -d "$DIR" ] || die "usage: feed-place <dir> | --remove"
BASE=$(head -n 1 "$DIR/base" 2>/dev/null | tr -d '\r')
printf '%s\n' "$BASE" | grep -Eq '^https://[A-Za-z0-9.-]+(:[0-9]{1,5})?(/[A-Za-z0-9._~-]+)*$' ||
	die "the software source address is missing or is not https: not adding it"
ARCH=$(arch)
printf '%s\n' "$ARCH" | grep -Eq '^[A-Za-z0-9_.-]+$' || die "cannot tell this router's processor type: not adding the software source"

if [ "$FMT" = apk ]; then
	KEY=$DIR/apk.pem
	[ -s "$KEY" ] || die "the signing key is not here: not adding the software source"
	LINE=$BASE/apk/$ARCH/packages.adb
	n=$(sha256sum < "$KEY" | cut -c1-8)
	revoked "$n" && die "this signing key was revoked by ForestVPN: not adding the software source"
	# a key with these exact bytes is already there: leave it; else write it under a name of ours
	have=
	for k in "$ETC"/apk/keys/forestvpn-*; do [ -f "$k" ] && cmp -s "$k" "$KEY" && have=1; done
	if [ -z "$have" ]; then
		mkdir -p "$ETC/apk/keys" || die "cannot write the key"
		cp "$KEY" "$ETC/apk/keys/forestvpn-apk-$n.pem" || die "cannot write the key"
	fi
	grep -qxsF "$LINE" "$ETC"/apk/repositories.d/*.list || append "$ETC/apk/repositories.d/customfeeds.list" "$LINE" || die "cannot write the software source"
else
	KEY=$DIR/ipk.pub
	[ -s "$KEY" ] || die "the signing key is not here: not adding the software source"
	LINE="src/gz forestvpn $BASE/ipk/$ARCH"
	command -v usign >/dev/null 2>&1 || die "this router has no usign: cannot name the key"
	fpr=$(usign -F -p "$KEY" 2>/dev/null)
	printf '%s\n' "$fpr" | grep -Eq '^[0-9a-f]{16}$' || die "the signing key is not a usign key"
	revoked "$fpr" && die "this signing key was revoked by ForestVPN: not adding the software source"
	# SEC-1 TM-002: the updater removes a revoked key only if it is on this list, so a signed release can never delete OpenWrt's own
	mkdir -p "$ETC/forestvpn" && { grep -qxsF "$fpr" "$ETC/forestvpn/placed_keys" || echo "$fpr" >> "$ETC/forestvpn/placed_keys"; } || die "cannot record the key"
	mkdir -p "$ETC/opkg/keys" || die "cannot write the key"
	cmp -s "$KEY" "$ETC/opkg/keys/$fpr" || cp "$KEY" "$ETC/opkg/keys/$fpr" || die "cannot write the key"
	grep -Eqs '^src/gz[ 	]+forestvpn[ 	]' "$ETC"/opkg/*.conf || append "$ETC/opkg/customfeeds.conf" "$LINE" || die "cannot write the software source"
fi
exit 0
