Shipping the app publishes the key
A mobile app or a single-page web app runs entirely on hardware you do not control. Every string compiled into the APK or IPA, and every value bundled into the JavaScript your CDN serves, is handed to each person who installs or loads it. The honest mental model is that shipping a client publishes its contents, source included. Even so, one of the most common defects in shipped software is a third-party API key sitting in the client, because the app needed to call a payments provider, a maps service, an email gateway or a language model API.
The reasoning that gets teams here is consistent. The key is compiled, so it feels hidden. The bundle is minified, so it feels unreadable. The value is fetched at runtime, so it feels absent from the artefact. None of this changes who holds the device. Whatever the wrapping, the key must exist in cleartext at the moment the app sets the request header, and the person holding the phone can observe that moment. Obfuscation raises the cost of extraction from minutes to hours. The key still comes out.
How keys come out of a binary
An Android package is an archive that standard decompilers turn back into readable code in minutes, and a plain string search over the binary surfaces anything with a recognisable shape. Most vendor keys have one: Google API keys start with AIza, AWS access key IDs with AKIA, Stripe secret keys with sk_live. iOS binaries take slightly more effort and yield to the same approach. Web apps are easier again, because the bundle is the delivery format. The key sits in the minified JavaScript, and if the build shipped source maps alongside it, the original variable names come too.
Keys fetched over the network rather than baked in fare little better. An intercepting proxy on a device the tester controls reads the value in transit. Certificate pinning makes that harder, and public instrumentation frameworks remove pinning on a rooted or jailbroken device in a scripted step. None of this is specialist tradecraft, and little of it is targeted at you specifically. Automated pipelines crawl app stores and public web bundles harvesting anything key-shaped, so a new release feeds that pipeline the day it goes out. GitHub and several vendors scan public code for leaked credentials and revoke what they find, which is a useful safety net that only fires for the patterns they know and the places they look.
What an attacker does with a harvested key
What happens next depends on what the key can do. Keys metered by usage get spent: language model tokens, SMS gateways and geocoding quotas all convert directly into someone else's workload on your invoice, and the first sign is often a billing alert or a suspended account. Keys that send get used for spam, which burns your sender reputation. Keys that read are the serious case. A key with reach into object storage, a database or a customer API can expose personal information, and under the Notifiable Data Breaches scheme in the Privacy Act 1988 that is a suspected eligible data breach: you have 30 days to assess it, and you must notify the OAIC and the affected individuals if the assessment finds a likely risk of serious harm, arriving through a channel your incident response plan probably never named.
It helps to separate identifiers that are designed to ship from secrets that never should. Stripe publishable keys, Firebase client configuration, analytics write keys and maps browser keys are built to appear in clients, and they are safe only when authorisation happens somewhere behind them. That is where the real findings live. A Firebase config inside an APK is expected; database security rules that let any signed-in user read every collection are the breach, and the pair shows up constantly in mobile assessments. For any client-side key, assume visibility and ask what the systems behind it allow a stranger holding it to do.
The backend-for-frontend pattern
The structural fix is to keep secrets on a server you control and give the client a narrow door. The app authenticates the user and calls your backend; the backend holds the vendor key, makes the vendor call, and applies everything the vendor cannot: per-user authorisation, rate limits, input constraints and logging tied to a real identity. This is the backend-for-frontend pattern. The vendor sees one well-behaved caller. You see every user, and you can cut any one of them off.
Some work genuinely needs the client to talk to a service directly, and the pattern extends cleanly. For uploads and downloads, the backend mints a presigned URL scoped to one object for a few minutes. For cloud APIs, it exchanges the user's session for short-lived, tightly scoped temporary credentials. For realtime services, it issues a token bound to the user and the channel. The design rule is the same in every case: nothing on the client should outlive the session, and nothing on the client should carry more scope than the signed-in user has earned.
Hiding the secret harder is the approach that fails. Splitting the key across strings, encoding it, pushing it into native code, or encrypting it with another key that also ships all raise extraction effort and leave the architecture intact. Device attestation, through Play Integrity on Android and App Attest on iOS, is worth adding because it makes scripted abuse from emulators and tampered builds more expensive. It verifies which app is calling, and everything inside that app remains readable by its user.
Scoping and rotation for keys that must ship
Some keys must ship, and for those the work is containment. Every serious vendor supports application restrictions: a maps or client API key can be bound to your Android package name and signing certificate fingerprint, your iOS bundle identifier, or your web origins, so a copied key fails when replayed from anywhere else. API restrictions narrow what the key may call to the one service the app actually uses. Quotas and budget alerts put a ceiling on what abuse can cost before a human looks. A shipped key with all three constraints is a nuisance to an attacker. A shipped key with none of them is a blank cheque.
Rotation needs planning before the first release, because mobile binaries live longer than web deploys. A key baked into the binary can only be replaced by shipping a new version, and old installs keep using the old key for years, so revoking it breaks real users. The workable pattern is to serve client keys through remote configuration fetched at runtime, which turns rotation into a configuration change while leaving the key exactly as visible as before. Pair it with a minimum supported version and a forced-update path, and hold one operating assumption: any key that has ever shipped is public forever, and rotation means revocation plus replacement.
Monitoring for abuse, and what a review checks
Extraction is silent, so detection happens on the usage side. Meter every key individually and alert on anomalies: request volumes no released app version could produce, calls from geographies or user agents that make no sense for your customer base, a key restricted to one API suddenly probing others. Vendor dashboards already carry most of this signal; the usual gap is that nobody routed it anywhere a human watches. Billing alerts are the cheapest tripwire you can set, and in practice they are often the first place key abuse surfaces.
When Black Shard reviews a mobile app or a frontend, the shipped artefact is the starting point: we pull the APK or the production bundle and read it the way an attacker would, then trace every embedded credential to what the systems behind it actually allow. Most findings in this area live in the architecture and the vendor configuration, so the fix conversation covers the backend, the key restrictions and the release process together. The checklist below is the self-assessment version. The full trace from artefact to blast radius, and the remediation sequencing around a live user base, is engagement work.
- Decompile your own APK and grep your production JavaScript and source maps for key-shaped strings, before someone else does it for you.
- Move every true secret behind a backend you control, and leave the client holding only short-lived credentials scoped to the signed-in user.
- Bind every key that must ship to your application identity (package name and certificate, bundle ID, or web origin) and restrict it to the one API it needs.
- Set quotas and budget alerts on every metered key so abuse has a price ceiling and a tripwire.
- Serve client keys from remote configuration so rotation becomes a configuration change, and keep a forced-update path for the day you need it.
- Treat every key that has ever shipped as public, and rotate on that assumption whenever a repository or a device you trusted is compromised.
