#!/usr/bin/env bash set -euo pipefail ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) usage() { cat >&2 <<'EOF' Usage: verify-play-installed-android.sh PLAY_IDENTITIES_JSON DEVICE_SERIAL EXPECTED_VERSION_CODE EXPECTED_VERSION_NAME Verifies, without changing the device, that org.whoneedhelp.mobile was installed by Google Play, is signed by one of the supplied Play App Signing SHA-256 identities, has the expected version, and owns the verified production App Link. EOF } if [[ $# -ne 4 ]]; then usage exit 2 fi identities_file=$1 device_serial=$2 expected_version_code=$3 expected_version_name=$4 package_name=org.whoneedhelp.mobile app_link_host=whoneedhelp.com app_link_url=https://whoneedhelp.com/safety expected_activity=org.whoneedhelp.mobile/.MainActivity adb_bin=${WNH_ADB_BIN:-adb} if [[ "$identities_file" != /* ]]; then identities_file="$ROOT/$identities_file" fi for command in jq sed tr; do command -v "$command" >/dev/null 2>&1 || { echo "Required command is unavailable: $command" >&2 exit 1 } done command -v "$adb_bin" >/dev/null 2>&1 || { echo "adb is unavailable: $adb_bin" >&2 exit 1 } [[ -f "$identities_file" && ! -L "$identities_file" ]] || { echo "Play identities must be a regular non-symlink file: $identities_file" >&2 exit 1 } case "$(stat -c '%a' "$identities_file")" in 400 | 600) ;; *) echo "Play identities must have mode 0400 or 0600: $identities_file" >&2 exit 1 ;; esac [[ -n "$device_serial" && "$device_serial" != *$'\n'* && "$device_serial" != *$'\r'* ]] || { echo "DEVICE_SERIAL must be a non-empty single-line value." >&2 exit 1 } [[ "$expected_version_code" =~ ^[1-9][0-9]*$ ]] || { echo "EXPECTED_VERSION_CODE must be a positive integer." >&2 exit 1 } [[ -n "$expected_version_name" && "$expected_version_name" != *$'\n'* && "$expected_version_name" != *$'\r'* ]] || { echo "EXPECTED_VERSION_NAME must be a non-empty single-line value." >&2 exit 1 } if ! jq --exit-status --arg package "$package_name" ' def valid_sha256: test("^[0-9A-Fa-f]{64}$|^([0-9A-Fa-f]{2}:){31}[0-9A-Fa-f]{2}$"); def normalized_sha256: ascii_upcase | gsub(":"; ""); (.package_name == $package) and (.identities | type == "array" and length > 0) and all( .identities[]; (.sha256 | type == "string" and valid_sha256) ) and (([.identities[].sha256 | normalized_sha256] | unique | length) == (.identities | length)) ' "$identities_file" >/dev/null; then echo "Play identities are incomplete, malformed, duplicated, or belong to another package." >&2 exit 1 fi mapfile -t expected_fingerprints < <( jq --raw-output '.identities[].sha256 | ascii_upcase | gsub(":"; "")' \ "$identities_file" ) adb_device() { "$adb_bin" -s "$device_serial" "$@" } [[ "$(adb_device get-state 2>/dev/null | tr -d '\r')" == device ]] || { echo "The selected Android device is not connected and authorised." >&2 exit 1 } package_path=$(adb_device shell pm path "$package_name" 2>/dev/null | tr -d '\r') [[ "$package_path" == package:* ]] || { echo "$package_name is not installed on the selected device." >&2 exit 1 } package_report=$(adb_device shell dumpsys package "$package_name") observed_version_code=$( sed -n 's/.*versionCode=\([0-9][0-9]*\).*/\1/p' <<<"$package_report" | head -n 1 ) observed_version_name=$( sed -n 's/^[[:space:]]*versionName=\(.*\)$/\1/p' <<<"$package_report" | head -n 1 | tr -d '\r' ) installer=$( sed -n 's/^[[:space:]]*installerPackageName=\(.*\)$/\1/p' <<<"$package_report" | head -n 1 | tr -d '\r' ) [[ "$observed_version_code" == "$expected_version_code" ]] || { echo "Installed versionCode does not match the expected Play release." >&2 exit 1 } [[ "$observed_version_name" == "$expected_version_name" ]] || { echo "Installed versionName does not match the expected Play release." >&2 exit 1 } [[ "$installer" == com.android.vending ]] || { echo "The installed package was not delivered by Google Play." >&2 exit 1 } links_report=$(adb_device shell pm get-app-links "$package_name") signature_line=$( sed -n 's/^[[:space:]]*Signatures: \[\(.*\)\][[:space:]]*$/\1/p' \ <<<"$links_report" | head -n 1 ) [[ -n "$signature_line" ]] || { echo "Android did not report a signing identity for the installed package." >&2 exit 1 } signature_match=false IFS=',' read -r -a observed_signatures <<<"$signature_line" for observed_signature in "${observed_signatures[@]}"; do observed_compact=$(printf '%s' "$observed_signature" | tr '[:lower:]' '[:upper:]' | tr -d ':[:space:]') for expected_fingerprint in "${expected_fingerprints[@]}"; do if [[ "$observed_compact" == "$expected_fingerprint" ]]; then signature_match=true break 2 fi done done [[ "$signature_match" == true ]] || { echo "The installed package is not signed by a supplied Play App Signing identity." >&2 exit 1 } if ! grep -Eq "^[[:space:]]+$app_link_host:[[:space:]]+verified[[:space:]]*$" \ <<<"$links_report"; then echo "The production Android App Link domain is not verified on the device." >&2 exit 1 fi resolved_activity=$( adb_device shell cmd package resolve-activity --brief \ -a android.intent.action.VIEW \ -c android.intent.category.BROWSABLE \ -d "$app_link_url" | tr -d '\r' ) if ! grep -Fx "$expected_activity" <<<"$resolved_activity" >/dev/null; then echo "The production App Link does not resolve to Who Need Help MainActivity." >&2 exit 1 fi echo "Google Play installed Android verification passed." echo "Package: $package_name" echo "Version: $observed_version_name ($observed_version_code)" echo "Installer: Google Play" echo "Signing identity: supplied Play App Signing set member" echo "App Link: $app_link_host verified and resolved to MainActivity"