AppForShare

Hardcoded API keys in an APK or IPA: how to find and fix them

· 7 min read

Hardcoded API keys in an APK or IPA: how to find and fix them

Every mobile developer has done it at least once: a key pasted into a constants file to get a feature working, with a note to move it later. The build goes to testers, then to the store, and the key goes with it. An .apk and an .ipa are zip archives. Anyone who has the file can unpack it and read the strings inside in a few minutes, with free tools and no special skill.

This guide shows how to look for hardcoded keys in your own builds, how to tell which ones matter, and what a proper fix looks like. It ends with how AppForShare checks every uploaded build for the most common key formats.

Why obfuscation does not hide keys

A common belief is that R8 on Android, or compiling Swift to machine code on iOS, hides a key. It does not. R8 renames classes and methods, but string constants stay as they are, because the app needs them at runtime. A Swift string literal sits in the binary in plain text. React Native bundles keep string literals readable, with or without Hermes. Flutter compiles Dart ahead of time, and the strings are still in the compiled code.

So the question is never "can someone find it", it is "what can they do with it once they have it".

Which keys are a real risk

Not every key in an app is a leak. Some are designed to ship in a client and are protected by restrictions on the provider's side. Others give full access to an account that bills you or holds customer data. The difference decides how urgent the fix is.

KeyWhat it looks likeRisk in an app
AWS access key idStarts with AKIA, usually next to a 40 character secretHigh: access to whatever the IAM user can do
Stripe live secret or restricted keysk_live_ or rk_live_High: charges, refunds, customer data
Private key-----BEGIN PRIVATE KEY----- or an RSA or EC variantHigh: whoever has it can sign or decrypt as you
Google service account JSON"type": "service_account" with private_key and client_emailHigh: server access to Google Cloud or Firebase
GitHub tokenghp_, gho_, github_pat_ and similarHigh: your repositories
Slack token or webhookxoxb-, xoxp-, or a hooks.slack.com/services/ addressMedium to high: post or read in your workspace
Legacy FCM server keyAAAA followed later by :APA91bHigh: push to every user; Google has retired the legacy API anyway
Google API keyStarts with AIzaLow when restricted: meant to ship, for example in google-services.json

A Stripe publishable key (pk_live_) is also meant for clients. So is a Firebase config. What protects those is not secrecy but the rules on the server side: Firebase Security Rules, App Check, and API restrictions on the Google Cloud key.

How to search your own build

Android APK

  1. Unzip it: unzip -o app-release.apk -d apk. You get classes*.dex, resources.arsc, assets/ and native libraries under lib/.
  2. Search the raw files: grep -r -a -E "AKIA[0-9A-Z]{16}|sk_live_|rk_live_|BEGIN [A-Z ]*PRIVATE KEY|ghp_|xox[bpa]-" apk/.
  3. For a readable view of the code, open the APK in jadx and search for key, secret, token and Bearer.
  4. Check assets/ by hand. Build scripts like to copy .env files, JSON configs and certificates into it.
  5. For React Native, look in assets/index.android.bundle. For Flutter, run strings on lib/arm64-v8a/libapp.so.

iOS IPA

  1. Unzip it: unzip -o MyApp.ipa -d ipa. The app is in ipa/Payload/MyApp.app.
  2. Read Info.plist with plutil -p Info.plist. Keys put there "for configuration" are as public as any other string.
  3. Run strings Payload/MyApp.app/MyApp | grep -E "AKIA|sk_live_|BEGIN" on the main binary.
  4. Check bundled JSON, plist and .p12 files, and main.jsbundle for React Native.

Expect some noise. Libraries ship sample keys in their docs and tests, and many strings look random. A finding is real when the format matches a known provider and the value works.

The fix is not deleting the line

Once a build with a real key has left your machine, assume the key is known. Removing it from the next build does not help, because the old build is still on testers' phones and in their downloads folders. The fix has three parts.

  1. Rotate the key now. Create a new one at the provider and revoke the old one. Check the provider's logs for use you do not recognise.
  2. Move the work to your server. The app calls your backend, the backend holds the key and calls the provider. For AWS, use short lived credentials from Cognito or your own token service instead of an IAM user key. For pushes, send from your server with the FCM HTTP v1 API.
  3. Restrict what has to ship. Limit Google API keys to your Android package name and signing certificate fingerprint, or your iOS bundle id, and to the APIs the app uses. Turn on Firebase App Check for the services that support it.

Then stop it from coming back: keep keys out of the repository with a secret scanner such as gitleaks or GitHub secret scanning, and check the built file as well, because build scripts can add files that were never committed.

Checking every build automatically

A manual search is fine once. Doing it on every test build is where it falls apart. AppForShare runs a security scan on every uploaded build, in the same step as the store readiness check. For keys, it reads dex code, resources, assets, React Native bundles, Info.plist, the iOS main binary and Flutter compiled code, and looks for the high risk formats in the table above. Google API keys show as a note with advice to restrict them, not as an error.

It keeps only the kind of key, the file it was found in and a masked value with the first 4 and last 4 characters. The full value is never stored, logged or sent to the AI fix plan. Values that look like samples, such as keys containing EXAMPLE, are skipped.

Free accounts see the counts on each build. Pro ($12 a month) and Team ($39 a month) show each finding with the file and the fix. In CI, fail the job when a build has something to fix:

npx appforshare upload app-release.apk
npx appforshare security <build id or slug> --fail-on block

The command exits with code 2 and the build stays uploaded. Details are in the security scan docs.

Be clear about the limits. It matches known formats on high confidence patterns only, so a key in a format it does not know is not found. It does not scan third party iOS frameworks for secrets. It is a static check of the file, not a penetration test.

Common questions

Is a Google API key in google-services.json a leak?

Not by itself. Firebase expects it in the app. Restrict the key in Google Cloud to your app and to the APIs you use, and protect your data with Security Rules. That is why the AppForShare scan marks it as a note.

Does ProGuard or R8 hide strings?

No. They shrink and rename code but keep string constants. String encryption tools exist, but the app still has to decrypt the value at runtime, so a determined person can read it. A key that must stay private belongs on a server.

We only sent the build to five testers. Do we still rotate?

Yes. Files get forwarded, backed up and left in download folders. Rotation is cheap compared with explaining an unexpected cloud bill.

What else should I check before sharing a build?

See the full list in security checks before you share an IPA or APK, and the wider picture in OWASP MASVS for small app teams.

If you share test builds anyway, let the scan look at each one for you. Create a free account, upload a build and see the counts on its build page.

Keep reading

App Distribution Platform · · 5 min read

AppForShare File Size and Upload Limits

AppForShare limits the size of each build you upload by plan: 50 MB without an account, 150 MB on the Free plan, 2 GB on Pro and 4 GB on Team. Accounts also have a storage total, and every plan...