#!/bin/sh
# Verify the release APK's identity, alignment, native payload, and signature.
set -eu

root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
# shellcheck source=versions
. "$root/versions"
apk=${1:-$root/app/build/outputs/apk/release/app-release-unsigned.apk}
[ -f "$apk" ] || { echo "verify-apk: not found: $apk" >&2; exit 1; }
command -v readelf >/dev/null 2>&1 || { echo "verify-apk: readelf is required" >&2; exit 1; }
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' 0 1 2 3 15

entries=$(unzip -Z1 "$apk")
actual=$(printf '%s\n' "$entries" | sed -n 's|^lib/\([^/]*\)/.*|\1|p' | sort -u | tr '\n' ' ' | sed 's/ $//')
expected=$(printf '%s\n' "$ABIS" | tr ' ' '\n' | sed '/^$/d' | sort -u | tr '\n' ' ' | sed 's/ $//')
[ "$actual" = "$expected" ] || {
	echo "verify-apk: native ABIs are '$actual', expected '$expected'" >&2
	exit 1
}
for abi in $ABIS; do
	for lib in libxrsync.so libxrsh.so; do
		path="lib/$abi/$lib"
		printf '%s\n' "$entries" | grep -Fqx "$path" || {
			echo "verify-apk: missing lib/$abi/$lib" >&2
			exit 1
		}
		elf="$tmp/$abi-$lib"
		unzip -p "$apk" "$path" > "$elf"
		header=$(readelf -W -h "$elf")
		printf '%s\n' "$header" | grep -Fq 'DYN (Position-Independent Executable file)' || {
			echo "verify-apk: $path is not PIE" >&2
			exit 1
		}
		case "$abi" in
		arm64-v8a) machine=AArch64 ;;
		x86_64) machine='Advanced Micro Devices X86-64' ;;
		*) echo "verify-apk: no machine check for $abi" >&2; exit 1 ;;
		esac
		actual_machine=$(printf '%s\n' "$header" | sed -n 's/^[[:space:]]*Machine:[[:space:]]*//p')
		[ "$actual_machine" = "$machine" ] || {
			echo "verify-apk: $path has the wrong machine type" >&2
			exit 1
		}
		program_headers=$(readelf -W -l "$elf")
		printf '%s\n' "$program_headers" | grep -Fq 'GNU_RELRO' || {
			echo "verify-apk: $path has no RELRO segment" >&2
			exit 1
		}
		load_alignment=$(printf '%s\n' "$program_headers" |
			awk '$1 == "LOAD" { print $NF }' | sort -u)
		[ "$load_alignment" = "0x4000" ] || {
			echo "verify-apk: $path LOAD alignment is '$load_alignment', expected 0x4000" >&2
			exit 1
		}
		stack=$(printf '%s\n' "$program_headers" | sed -n '/GNU_STACK/p')
		if [ -z "$stack" ] || printf '%s\n' "$stack" | grep -Eq 'RWE|RWX'; then
			echo "verify-apk: $path has an executable or missing stack marker" >&2
			exit 1
		fi
		dynamic=$(readelf -W -d "$elf")
		printf '%s\n' "$dynamic" | grep -Fq 'BIND_NOW' || {
			echo "verify-apk: $path does not use immediate binding" >&2
			exit 1
		}
		! printf '%s\n' "$dynamic" | grep -Eq 'RPATH|RUNPATH|TEXTREL' || {
			echo "verify-apk: $path has an unsafe dynamic-linker flag" >&2
			exit 1
		}
		needed=$(printf '%s\n' "$dynamic" | sed -n 's/.*Shared library: \[\(.*\)\]/\1/p' | LC_ALL=C sort)
		if [ "$lib" = libxrsh.so ]; then
			expected_needed=$(printf '%s\n' libc.so libdl.so liblog.so | LC_ALL=C sort)
		else
			expected_needed=$(printf '%s\n' libc.so libdl.so | LC_ALL=C sort)
		fi
		[ "$needed" = "$expected_needed" ] || {
			echo "verify-apk: $path system libraries changed:$needed" >&2
			exit 1
		}
		sections=$(readelf -W -S "$elf")
		! printf '%s\n' "$sections" | grep -Eq '\.debug_|\.symtab' || {
			echo "verify-apk: $path is not stripped" >&2
			exit 1
		}
		if [ "$lib" = libxrsync.so ]; then
			symbols=$(readelf -W -s "$elf")
			printf '%s\n' "$symbols" | grep -Fq '__stack_chk_fail' || {
				echo "verify-apk: $path has no stack protector" >&2
				exit 1
			}
			printf '%s\n' "$symbols" | grep -Fq '__memcpy_chk' || {
				echo "verify-apk: $path has no fortified libc calls" >&2
				exit 1
			}
		fi
	done
done
native_entries=$(printf '%s\n' "$entries" | sed -n '/^lib\//p')
unexpected=
if unexpected=$(printf '%s\n' "$native_entries" |
	grep -Ev '^lib/[^/]+/(libxrsync|libxrsh)\.so$'); then
	:
else
	status=$?
	[ "$status" -eq 1 ] || exit "$status"
fi
[ -z "$unexpected" ] || { echo "verify-apk: unexpected native files:$unexpected" >&2; exit 1; }
! printf '%s\n' "$entries" | grep -Fqx META-INF/version-control-info.textproto || {
	echo "verify-apk: APK contains a VCS stamp" >&2
	exit 1
}
for notice in assets/APACHE-2.0 assets/LICENSE assets/THIRD_PARTY; do
	printf '%s\n' "$entries" | grep -Fqx "$notice" || {
		echo "verify-apk: missing $notice" >&2
		exit 1
	}
	asset="$tmp/$(basename "$notice")"
	unzip -p "$apk" "$notice" > "$asset"
	cmp "$asset" "$root/$(basename "$notice")"
done

zipalign -c -P 16 4 "$apk" >/dev/null
badging=$(aapt dump badging "$apk")
printf '%s\n' "$badging" | grep -Fq "package: name='invalid.lena.rsend' versionCode='$APP_VERSION_CODE' versionName='$APP_VERSION'" || {
	echo "verify-apk: package version does not match versions" >&2
	exit 1
}
printf '%s\n' "$badging" | grep -Fqx "sdkVersion:'$ANDROID_MIN_SDK'" || {
	echo "verify-apk: minimum SDK does not match versions" >&2
	exit 1
}
printf '%s\n' "$badging" | grep -Fqx "targetSdkVersion:'$ANDROID_TARGET_SDK'" || {
	echo "verify-apk: target SDK does not match versions" >&2
	exit 1
}
! printf '%s\n' "$badging" | grep -Fq 'application-debuggable' || {
	echo "verify-apk: release APK is debuggable" >&2
	exit 1
}
permission_dump=$(aapt dump permissions "$apk")
permissions=$(printf '%s\n' "$permission_dump" |
	sed -n "s/^uses-permission: name='\(.*\)'$/\1/p" | LC_ALL=C sort)
expected_permissions=$(cat <<EOF | LC_ALL=C sort
android.permission.ACCESS_NETWORK_STATE
android.permission.FOREGROUND_SERVICE
android.permission.FOREGROUND_SERVICE_DATA_SYNC
android.permission.INTERNET
android.permission.MANAGE_EXTERNAL_STORAGE
android.permission.POST_NOTIFICATIONS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
android.permission.WAKE_LOCK
invalid.lena.rsend.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION
EOF
)
[ "$permissions" = "$expected_permissions" ] || {
	echo "verify-apk: permission set changed:$permissions" >&2
	exit 1
}

signature="$tmp/signature"
if apksigner verify --min-sdk-version "$ANDROID_MIN_SDK" --verbose "$apk" >"$signature" 2>&1; then
	case "$(basename "$apk")" in
	*-unsigned.apk)
		echo "verify-apk: unsigned APK unexpectedly has a valid signature" >&2
		exit 1
		;;
	esac
	# At API 30 apksigner reports only v3 as used, even when valid v1 and v2
	# signatures are present. API 23 makes it verify v1 for that platform and
	# v2/v3 for later platforms in the same pass.
	schemes="$tmp/schemes"
	apksigner verify --min-sdk-version 23 --verbose "$apk" >"$schemes" 2>&1
	grep -Fq 'Verified using v1 scheme (JAR signing): true' "$schemes"
	grep -Fq 'Verified using v2 scheme (APK Signature Scheme v2): true' "$schemes"
	grep -Fq 'Verified using v3 scheme (APK Signature Scheme v3): true' "$schemes"
	echo "verify-apk: signed APK verified (v1, v2, v3)"
else
	case "$(basename "$apk")" in
	*-unsigned.apk) ;;
	*)
		cat "$signature" >&2
		echo "verify-apk: signed APK failed verification" >&2
		exit 1
		;;
	esac
	! printf '%s\n' "$entries" | grep -Eq '^META-INF/.*\.(RSA|DSA|EC)$' || {
		echo "verify-apk: unsigned APK contains a signature file" >&2
		exit 1
	}
	echo "verify-apk: unsigned APK"
fi

echo "verify-apk: ok"
