#!/usr/bin/env bash set -euo pipefail ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) usage() { cat >&2 <<'EOF' Usage: import-play-android-config.sh ENV_FILE GOOGLE_SERVICES_JSON PLAY_IDENTITIES_JSON [--apply] The command validates a production Google/Firebase Android client against the Play App Signing certificate identities without changing ENV_FILE by default. Use --apply only after reviewing the plan. PLAY_IDENTITIES_JSON must contain: { "package_name": "org.whoneedhelp.mobile", "identities": [ {"sha1": "AA:...", "sha256": "BB:..."} ] } EOF } if [[ $# -lt 3 || $# -gt 4 ]]; then usage exit 2 fi env_file=$1 google_services_file=$2 identities_file=$3 mode=${4:-} if [[ -n "$mode" && "$mode" != --apply ]]; then usage exit 2 fi for path_variable in env_file google_services_file identities_file; do path=${!path_variable} if [[ "$path" != /* ]]; then printf -v "$path_variable" '%s/%s' "$ROOT" "$path" fi done for command in awk jq mktemp stat; do command -v "$command" >/dev/null 2>&1 || { echo "Required command is unavailable: $command" >&2 exit 1 } done [[ -f "$env_file" ]] || { echo "Production environment does not exist: $env_file" >&2 exit 1 } [[ "$(stat -c '%a' "$env_file")" == 600 ]] || { echo "Production environment must have mode 0600: $env_file" >&2 exit 1 } for provider_file in "$google_services_file" "$identities_file"; do [[ -f "$provider_file" ]] || { echo "Required provider input does not exist: $provider_file" >&2 exit 1 } case "$(stat -c '%a' "$provider_file")" in 400 | 600) ;; *) echo "Provider inputs must have mode 0400 or 0600: $provider_file" >&2 exit 1 ;; esac done read_unique() { local key=$1 awk -v key="$key" ' index($0, key "=") == 1 { count += 1 value = substr($0, length(key) + 2) } END { if (count != 1) exit 1 print value } ' "$env_file" || { echo "$key must occur exactly once in $env_file." >&2 exit 1 } } deployment_env=$(read_unique DEPLOYMENT_ENV) package_name=$(read_unique ANDROID_APP_LINKS_PACKAGE_NAME) existing_app_links=$(read_unique ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS) existing_play_fingerprints=$(read_unique ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS) existing_authorized_parties=$(read_unique GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS) existing_firebase_project=$(read_unique WNH_FIREBASE_PROJECT_ID) existing_fcm_project=$(read_unique FCM_PROJECT_ID) [[ "$deployment_env" == production ]] || { echo "Play App Signing identities may only be imported into DEPLOYMENT_ENV=production." >&2 exit 1 } [[ "$package_name" == org.whoneedhelp.mobile ]] || { echo "The production Android package must be org.whoneedhelp.mobile." >&2 exit 1 } [[ -n "$existing_app_links" ]] || { echo "The production App Links list must already contain the measured upload certificate." >&2 exit 1 } [[ -n "$existing_authorized_parties" ]] || { echo "The production environment must already contain its Android OAuth authorized party." >&2 exit 1 } normalized_identities=$(mktemp "${TMPDIR:-/tmp}/wnh-play-identities.XXXXXX") matched_oauth_ids=$(mktemp "${TMPDIR:-/tmp}/wnh-play-oauth-ids.XXXXXX") values_file=$(mktemp "${TMPDIR:-/tmp}/wnh-play-values.XXXXXX") env_dir=$(CDPATH='' cd -- "$(dirname -- "$env_file")" && pwd) env_name=$(basename -- "$env_file") candidate_env=$(mktemp "$env_dir/$env_name.play-candidate.XXXXXX") cleanup() { rm -f "$normalized_identities" "$matched_oauth_ids" "$values_file" "$candidate_env" } trap cleanup EXIT HUP INT TERM chmod 600 "$normalized_identities" "$matched_oauth_ids" "$values_file" "$candidate_env" if ! jq --exit-status --arg package "$package_name" ' def normalize_sha1: ascii_upcase | gsub(":"; ""); def normalize_sha256: ascii_upcase | gsub(":"; ""); def valid_sha1: test("^[0-9A-Fa-f]{40}$|^([0-9A-Fa-f]{2}:){19}[0-9A-Fa-f]{2}$"); def valid_sha256: test("^[0-9A-Fa-f]{64}$|^([0-9A-Fa-f]{2}:){31}[0-9A-Fa-f]{2}$"); (.package_name == $package) and (.identities | type == "array" and length > 0) and all( .identities[]; (.sha1 | type == "string" and valid_sha1) and (.sha256 | type == "string" and valid_sha256) ) and (([.identities[].sha1 | normalize_sha1] | unique | length) == (.identities | length)) and (([.identities[].sha256 | normalize_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 jq --compact-output ' { package_name, identities: [ .identities[] | { sha1: (.sha1 | ascii_upcase | gsub(":"; "")), sha256: (.sha256 | ascii_upcase | gsub(":"; "")) } ] } ' "$identities_file" >"$normalized_identities" if ! jq --exit-status --arg package "$package_name" ' (.project_info.project_number) as $project_number | [ .client[]? | select(.client_info.android_client_info.package_name == $package) ] as $clients | ($clients | length == 1) and (.project_info.project_id | type == "string" and length > 0 and test("^[^\r\n]+$")) and (.project_info.project_number | type == "string" and length > 0 and test("^[0-9]+$")) and ($clients[0].client_info.mobilesdk_app_id | type == "string" and length > 0 and startswith("1:" + $project_number + ":android:")) and ($clients[0].api_key[0].current_key | type == "string" and length > 0 and test("^[^\r\n]+$")) ' "$google_services_file" >/dev/null; then echo "Firebase configuration does not contain exactly one complete production Android client." >&2 exit 1 fi if ! jq --exit-status --raw-output \ --slurpfile identity_documents "$normalized_identities" \ --arg package "$package_name" ' def normalize_sha1: ascii_upcase | gsub(":"; ""); $identity_documents[0] as $identities | [ .client[] | select(.client_info.android_client_info.package_name == $package) | .oauth_client[]? | select(.client_type == 1) | select(.android_info.package_name == $package) | { client_id, sha1: (.android_info.certificate_hash | normalize_sha1) } ] as $android_clients | [ $identities.identities[] | . as $identity | [$android_clients[] | select(.sha1 == $identity.sha1)] as $matches | if ($matches | length) == 1 then $matches[0].client_id else error("each Play SHA-1 must match exactly one Android OAuth client") end ] as $matched | if (($matched | length) == ($identities.identities | length)) and (($matched | unique | length) == ($matched | length)) then $matched[] else error("Play Android OAuth clients are incomplete or duplicated") end ' "$google_services_file" >"$matched_oauth_ids"; then echo "Firebase configuration does not contain one distinct Android OAuth client for every Play SHA-1." >&2 exit 1 fi firebase_project=$(jq --raw-output '.project_info.project_id' "$google_services_file") if [[ -n "$existing_firebase_project" && "$existing_firebase_project" != "$firebase_project" ]]; then echo "Firebase download belongs to a different project than WNH_FIREBASE_PROJECT_ID." >&2 exit 1 fi if [[ -n "$existing_fcm_project" && "$existing_fcm_project" != "$firebase_project" ]]; then echo "Firebase download belongs to a different project than FCM_PROJECT_ID." >&2 exit 1 fi jq --null-input --raw-output \ --arg existing_app_links "$existing_app_links" \ --arg existing_play "$existing_play_fingerprints" \ --arg existing_authorized "$existing_authorized_parties" \ --slurpfile identity_documents "$normalized_identities" \ --rawfile matched_oauth "$matched_oauth_ids" \ --slurpfile firebase_documents "$google_services_file" ' def stable_unique: reduce .[] as $item ([]; if index($item) then . else . + [$item] end); def compact_fingerprint: ascii_upcase | gsub(":"; ""); def colonize: [range(0; length; 2) as $offset | .[$offset:$offset + 2]] | join(":"); def trim: gsub("^[[:space:]]+|[[:space:]]+$"; ""); $identity_documents[0] as $identities | $firebase_documents[0] as $firebase | ($existing_app_links | split(",") | map(trim | compact_fingerprint) ) as $published | if all($published[]; test("^[0-9A-F]{64}$")) then . else error("existing App Links fingerprints are malformed") end | ($existing_play | if length == 0 then [] else split(",") | map(trim | compact_fingerprint) end ) as $existing_play_values | if all($existing_play_values[]; test("^[0-9A-F]{64}$")) then . else error("existing Play fingerprints are malformed") end | ($existing_authorized | split(",") | map(trim)) as $authorized | if all($authorized[]; length > 0 and test("^[^\r\n,]+$")) then . else error("existing Android OAuth clients are malformed") end | ($identities.identities | map(.sha256)) as $new_play | ($matched_oauth | split("\n") | map(select(length > 0))) as $new_oauth | (($published + $new_play) | stable_unique | map(colonize)) as $all_published | (($existing_play_values + $new_play) | stable_unique | map(colonize)) as $all_play | (($authorized + $new_oauth) | stable_unique) as $all_authorized | ($firebase.client[] | select(.client_info.android_client_info.package_name == "org.whoneedhelp.mobile")) as $client | "ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=\($all_published | join(","))", "ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=\($all_play | join(","))", "GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS=\($all_authorized | join(","))", "WNH_FIREBASE_APPLICATION_ID=\($client.client_info.mobilesdk_app_id)", "WNH_FIREBASE_API_KEY=\($client.api_key[0].current_key)", "WNH_FIREBASE_PROJECT_ID=\($firebase.project_info.project_id)", "WNH_FIREBASE_GCM_SENDER_ID=\($firebase.project_info.project_number)" ' >"$values_file" cp "$env_file" "$candidate_env" chmod 600 "$candidate_env" "$ROOT/scripts/set-env-values.sh" "$candidate_env" "$values_file" >/dev/null "$ROOT/scripts/validate-android-environment.sh" "$candidate_env" production >/dev/null identity_count=$(jq '.identities | length' "$normalized_identities") published_count=$( awk -F= ' $1 == "ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS" { value = substr($0, index($0, "=") + 1) print split(value, fingerprints, ",") exit } ' "$candidate_env" ) authorized_count=$( awk -F= ' $1 == "GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS" { value = substr($0, index($0, "=") + 1) print split(value, clients, ",") exit } ' "$candidate_env" ) echo "Validated production Play Android identity import." echo "Target environment: $env_file" echo "Package: $package_name" echo "Play signing identities supplied: $identity_count" echo "Published App Links identities after import: $published_count" echo "Authorized Android OAuth clients after import: $authorized_count" echo "Firebase/FCM project relationship: verified" if [[ "$mode" == --apply ]]; then mv "$candidate_env" "$env_file" echo "Applied the validated values atomically without printing credentials." else echo "Plan only: no files were changed. Re-run with --apply after reviewing these counts." fi