Hardcoded API keys in an APK or IPA: how to find and fix them
· 7 min read
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.
| Key | What it looks like | Risk in an app |
|---|---|---|
| AWS access key id | Starts with AKIA, usually next to a 40 character secret | High: access to whatever the IAM user can do |
| Stripe live secret or restricted key | sk_live_ or rk_live_ | High: charges, refunds, customer data |
| Private key | -----BEGIN PRIVATE KEY----- or an RSA or EC variant | High: whoever has it can sign or decrypt as you |
| Google service account JSON | "type": "service_account" with private_key and client_email | High: server access to Google Cloud or Firebase |
| GitHub token | ghp_, gho_, github_pat_ and similar | High: your repositories |
| Slack token or webhook | xoxb-, xoxp-, or a hooks.slack.com/services/ address | Medium to high: post or read in your workspace |
| Legacy FCM server key | AAAA followed later by :APA91b | High: push to every user; Google has retired the legacy API anyway |
| Google API key | Starts with AIza | Low 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
- Unzip it:
unzip -o app-release.apk -d apk. You getclasses*.dex,resources.arsc,assets/and native libraries underlib/. - 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/. - For a readable view of the code, open the APK in jadx and search for
key,secret,tokenandBearer. - Check
assets/by hand. Build scripts like to copy.envfiles, JSON configs and certificates into it. - For React Native, look in
assets/index.android.bundle. For Flutter, runstringsonlib/arm64-v8a/libapp.so.
iOS IPA
- Unzip it:
unzip -o MyApp.ipa -d ipa. The app is inipa/Payload/MyApp.app. - Read
Info.plistwithplutil -p Info.plist. Keys put there "for configuration" are as public as any other string. - Run
strings Payload/MyApp.app/MyApp | grep -E "AKIA|sk_live_|BEGIN"on the main binary. - Check bundled JSON, plist and
.p12files, andmain.jsbundlefor 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.
- 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.
- 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.
- 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.


