Files
mail/scripts/verify-android-apk.sh
T

102 lines
4.8 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
project_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
apk="${1:-$project_dir/target/android/release/apk/flectar-mail.apk}"
signing_mode="${2:-test}"
android_sdk="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
if [[ ! -f "$apk" ]]; then
printf 'APK not found: %s\n' "$apk" >&2
exit 1
fi
if [[ -z "$android_sdk" || ! -d "$android_sdk/build-tools" ]]; then
printf 'Android SDK build-tools not found. Set ANDROID_HOME.\n' >&2
exit 1
fi
build_tools="$(find "$android_sdk/build-tools" -mindepth 1 -maxdepth 1 -type d -print | sort -V | tail -n 1)"
aapt="$build_tools/aapt"
apksigner="$build_tools/apksigner"
if [[ ! -x "$aapt" || ! -x "$apksigner" ]]; then
printf 'aapt/apksigner not found under %s\n' "$build_tools" >&2
exit 1
fi
badging="$($aapt dump badging "$apk")"
xmltree="$($aapt dump xmltree "$apk" AndroidManifest.xml)"
assert_contains() {
local haystack="$1"
local needle="$2"
local description="$3"
if [[ "$haystack" != *"$needle"* ]]; then
printf 'APK verification failed: %s (missing %s)\n' "$description" "$needle" >&2
exit 1
fi
}
assert_contains "$badging" "package: name='com.flectar.mail'" 'package identity'
if [[ -n "${FLECTAR_ANDROID_VERSION_NAME:-}" ]]; then
assert_contains "$badging" "versionName='$FLECTAR_ANDROID_VERSION_NAME'" 'release version name'
fi
if [[ "$signing_mode" == production && -n "${FLECTAR_ANDROID_VERSION_NAME:-}" ]]; then
expected_code="$(python3 "$project_dir/scripts/android-version-code.py" "$FLECTAR_ANDROID_VERSION_NAME")"
assert_contains "$badging" "versionCode='$expected_code'" 'shared release version code'
elif [[ -n "${FLECTAR_ANDROID_VERSION_CODE:-}" ]]; then
assert_contains "$badging" "versionCode='$FLECTAR_ANDROID_VERSION_CODE'" 'release version code'
fi
assert_contains "$badging" "sdkVersion:'26'" 'minimum SDK'
assert_contains "$badging" "targetSdkVersion:'36'" 'target SDK'
assert_contains "$badging" "uses-permission: name='android.permission.INTERNET'" 'network permission'
assert_contains "$badging" "uses-permission: name='android.permission.ACCESS_NETWORK_STATE'" 'network-state permission'
assert_contains "$badging" "uses-permission: name='android.permission.POST_NOTIFICATIONS'" 'notification permission declaration'
assert_contains "$badging" "native-code: 'arm64-v8a'" 'arm64 ABI'
assert_contains "$badging" 'application-icon-' 'launcher icon'
assert_contains "$xmltree" 'android:usesCleartextTraffic(0x010104ec)=(type 0x12)0x0' 'cleartext traffic disabled'
assert_contains "$xmltree" 'android:scheme(0x01010027)="msauth"' 'Microsoft OAuth callback scheme'
assert_contains "$xmltree" 'android:host(0x01010028)="com.flectar.mail"' 'Microsoft OAuth callback host'
assert_contains "$xmltree" 'android:exported(0x01010010)=(type 0x12)0xffffffff' 'explicitly exported callback/launcher Activity'
assert_contains "$xmltree" 'com.flectar.mail.FlectarActivity' 'Google AuthorizationClient Activity host'
assert_contains "$xmltree" 'android:allowBackup(0x01010280)=(type 0x12)0x0' 'Android backup disabled'
if [[ "$badging" == *"application-debuggable"* ]]; then
printf 'APK verification failed: release application is debuggable\n' >&2
exit 1
fi
if [[ "$badging" == *"'x86'"* || "$badging" == *"'x86_64'"* ]]; then
printf 'APK verification failed: release artifact contains an emulator ABI\n' >&2
exit 1
fi
archive_listing="$(unzip -l "$apk")"
if [[ "$archive_listing" != *'classes.dex'* ]]; then
printf 'APK verification failed: Google Play Services bridge classes are absent\n' >&2
exit 1
fi
python3 "$project_dir/scripts/verify-android-native.py" "$apk"
signing="$($apksigner verify --verbose --print-certs "$apk")"
printf '%s\n' "$signing"
signing_args=("$signing_mode")
if [[ "$signing_mode" == production || "${CI:-}" == true || "${GITHUB_ACTIONS:-}" == true ]]; then
# Published previews and stable releases must match the same committed
# certificate. Verification needs no access to the private key or password.
expected_sha256="$(cat "$project_dir/platform/android/app-signing-certificate.sha256")"
elif [[ "$signing_mode" == test ]]; then
data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
keystore="${FLECTAR_ANDROID_TEST_KEYSTORE:-$data_home/flectar-mail/android/test-signing.keystore}"
if [[ ! -f "$keystore" ]]; then
printf 'Expected test keystore not found: %s\n' "$keystore" >&2
exit 1
fi
# Local development keys are unrelated to the published app identity.
expected_sha256="$(keytool -exportcert -keystore "$keystore" -alias androiddebugkey -storepass android | sha256sum)"
else
printf 'Unknown Android signing mode: %s\n' "$signing_mode" >&2
exit 1
fi
signing_args+=(--expected-sha256 "${expected_sha256%% *}")
printf '%s\n' "$signing" | python3 "$project_dir/scripts/verify-android-signing.py" "${signing_args[@]}"
printf 'Verified Android package metadata, arm64 ABI, TLS policy, and signing: %s\n' "$apk"