186 lines
5.7 KiB
Bash
Executable File
186 lines
5.7 KiB
Bash
Executable File
#!/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.DEFAULT \
|
|
-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"
|