From ae56037e415a787b3623b26559868a8edc8d64ad Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Tue, 10 Nov 2026 18:00:16 +0100 Subject: [PATCH] Fix GMS gate APK mode false positive Decide by AGP library marker files instead of dex strings: play-services-base/basement reference GoogleSignInAccount without linking play-services-auth. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WzxdWVQE5pYiASpRGgA3Kg --- scripts/check_no_gms_signin.sh | 39 ++++++++++----- scripts/tests/test_check_no_gms_signin.py | 61 ++++++++++++++++++----- 2 files changed, 75 insertions(+), 25 deletions(-) diff --git a/scripts/check_no_gms_signin.sh b/scripts/check_no_gms_signin.sh index 67f4b41..0fd2dc1 100755 --- a/scripts/check_no_gms_signin.sh +++ b/scripts/check_no_gms_signin.sh @@ -14,8 +14,8 @@ # (google_sign_in_web, google_sign_in_platform_interface and their transitive # google_identity_services_web) are allowed while google_sign_in_android is # absent: kartka's web build uses them for the GIS button and they never -# reach an APK. APK mode greps the built dex for the GMS auth classes, the -# ground truth when a dependency arrives some other way. +# reach an APK. APK mode looks for the GMS auth library marker files in the +# built APK, the ground truth when a dependency arrives some other way. # # Usage: check_no_gms_signin.sh [pubspec.lock | dir ...] # check_no_gms_signin.sh --apk app.apk @@ -35,7 +35,19 @@ readonly WEB_ONLY=(google_sign_in_web google_sign_in_platform_interface # Anchored so com.google.gms:google-services (Firebase) and # play-services-auth-api-phone do not trip it. readonly GRADLE_PATTERN='com\.google\.android\.gms:play-services-(auth|identity-credentials)([^-[:alnum:]]|$)|com\.google\.android\.libraries\.identity\.googleid|androidx\.credentials:credentials-play-services-auth|name[[:space:]]*=[[:space:]]*"(play-services-auth|play-services-identity-credentials|credentials-play-services-auth|googleid)"' -readonly DEX_PATTERN='Lcom/google/android/(gms/auth|libraries/identity/googleid)/' +# APK mode: library marker files AGP copies to the APK root (or META-INF), +# one per linked artifact. Dex strings are not used: play-services-base and +# -basement reference GoogleSignInAccount without linking play-services-auth. +# play-services-fido (passkeys) is deliberately allowed. +readonly APK_MARKERS=( + googleid.properties + play-services-auth.properties + play-services-auth-base.properties + play-services-auth-api-phone.properties + play-services-auth-blockstore.properties + play-services-identity-credentials.properties + META-INF/androidx.credentials_credentials-play-services-auth.version +) readonly PRUNE=(-name build -o -name .dart_tool -o -name .git -o -name .gradle -o -name .claude -o -name node_modules) @@ -122,17 +134,18 @@ check_lock() { } check_apk() { - local apk="$1" dex count + local apk="$1" entries found=() + local marker [[ -f "$apk" ]] || { echo "Error: no such APK: $apk" >&2; exit 2; } - count="$(for dex in $(unzip -Z1 "$apk" 'classes*.dex'); do - unzip -p "$apk" "$dex" - done | strings | grep -cE "$DEX_PATTERN" || true)" - if [[ "$count" -gt 0 ]]; then - echo "Error: $apk has $count GMS sign-in class references" >&2 - echo " (Lcom/google/android/gms/auth or .../identity/googleid)." >&2 - fix_hint - return 1 - fi + entries="$(unzip -Z1 "$apk")" + for marker in "${APK_MARKERS[@]}"; do + grep -qxF -- "$marker" <<<"$entries" && found+=("$marker") + done + [[ "${#found[@]}" -eq 0 ]] && return 0 + echo "Error: $apk bundles Google's non-free sign-in libraries:" >&2 + printf ' %s\n' "${found[@]}" >&2 + fix_hint + return 1 } # Expands the arguments into pubspec.lock paths, NUL-separated. diff --git a/scripts/tests/test_check_no_gms_signin.py b/scripts/tests/test_check_no_gms_signin.py index 312f4c9..784e80f 100644 --- a/scripts/tests/test_check_no_gms_signin.py +++ b/scripts/tests/test_check_no_gms_signin.py @@ -136,23 +136,60 @@ def test_missing_explicit_file_is_a_usage_error(tmp_path: Path) -> None: assert _run(tmp_path / "nope.lock").returncode == 2 -def _apk(path: Path, dex: bytes) -> Path: +CREDENTIALS_MARKER = "META-INF/androidx.credentials_credentials-play-services-auth.version" +DEX_REF = b"\x00Lcom/google/android/gms/auth/api/signin/GoogleSignInAccount;\x00" * 2 + + +def _apk(path: Path, dex: bytes = b"", markers: tuple[str, ...] = ()) -> Path: + """Fake APK: dex strings plus library marker files AGP copies in.""" with zipfile.ZipFile(path, "w") as apk: apk.writestr("AndroidManifest.xml", b"\x03\x00") apk.writestr("classes.dex", b"dex\n035\x00" + dex) apk.writestr("classes2.dex", b"dex\n035\x00Lkotlin/Unit;\x00") + for name in markers: + apk.writestr(name, b"version=1") return path -def test_apk_with_gms_auth_classes_fails(tmp_path: Path) -> None: - dex = b"\x00Lcom/google/android/gms/auth/api/signin/GoogleSignIn;\x00" * 2 - proc = _run("--apk", _apk(tmp_path / "bad.apk", dex)) - assert proc.returncode == 1 - assert "2" in proc.stderr - assert "google_account_token" in proc.stderr - - -def test_apk_without_gms_auth_classes_passes(tmp_path: Path) -> None: - dex = b"\x00Lcom/google/android/gms/common/api/Status;\x00" - proc = _run("--apk", _apk(tmp_path / "good.apk", dex)) +def test_apk_with_only_dex_references_passes(tmp_path: Path) -> None: + """play-services-base references GoogleSignInAccount without linking auth.""" + markers = ( + "play-services-base.properties", + "play-services-basement.properties", + "play-services-tasks.properties", + "play-services-fido.properties", + ) + proc = _run("--apk", _apk(tmp_path / "ok.apk", DEX_REF, markers)) assert proc.returncode == 0, proc.stderr + + +def test_apk_without_markers_passes(tmp_path: Path) -> None: + assert _run("--apk", _apk(tmp_path / "clean.apk")).returncode == 0 + + +def test_apk_banned_markers_fail_and_are_named(tmp_path: Path) -> None: + banned = ( + "play-services-auth.properties", + "googleid.properties", + "play-services-auth-base.properties", + "play-services-auth-api-phone.properties", + "play-services-auth-blockstore.properties", + "play-services-identity-credentials.properties", + CREDENTIALS_MARKER, + ) + for marker in banned: + apk = _apk(tmp_path / "bad.apk", DEX_REF, ("play-services-base.properties", marker)) + proc = _run("--apk", apk) + assert proc.returncode == 1, marker + assert marker in proc.stderr + assert "google_account_token" in proc.stderr + + +def test_apk_nested_marker_name_is_not_a_root_marker(tmp_path: Path) -> None: + """Only APK-root files count; a same-named file in a subdir is not AGP's.""" + apk = _apk(tmp_path / "nested.apk", markers=("assets/play-services-auth.properties",)) + assert _run("--apk", apk).returncode == 0 + + +def test_apk_missing_file_is_a_usage_error(tmp_path: Path) -> None: + assert _run("--apk", tmp_path / "nope.apk").returncode == 2