Dunicot A cybersecurity consultancy and advisory firm.

Attack scenario · Telecommunications

A hardcoded API key in a mobile app is a public API key

A production API key compiled into a published mobile app can be extracted from the build and replayed against the API directly, with no app and no account.

High Hardcoded credentials and insecure local storage in mobile applications

At a glance

Vulnerability class
Use of hard-coded credentials and insecure local data storage, with an authorisation defect behind it wherever the recovered credential is honoured for an arbitrary subscriber
CWE
CWE-798 for the embedded credential, CWE-321 for a hard-coded signing key, CWE-312 for cleartext storage in the app sandbox, and CWE-639 or CWE-862 for the authorisation step that turns one recovered key into access to another subscriber's account
OWASP Mobile Top 10 2024
M1 Improper Credential Usage for the shipped key, M7 Insufficient Binary Protections for the extraction, M9 Insecure Data Storage for the sandbox artefacts, and M3 Insecure Authentication/Authorization for the step where a service credential is accepted as sole authority
Reachable from
Two tiers. The published store build alone, with no account, no credentials and no access to a victim's handset, gives up Android bytecode, resources and manifest entries, and on iOS the bundle resources, embedded property lists and JavaScript bundles, because store encryption covers only the main executable. A handset the attacker owns is needed for strings inside that executable, for instrumenting a string-decryption or key-derivation routine, and for anything in the app sandbox.
Privilege obtained
Whatever the recovered credential authenticates to, established endpoint by endpoint against a negative control. Where it authenticates with no user session and reaches subscriber data or provisioning, it is a service credential sitting in software that anyone can read.
Who is affected
Directly, every handset running the published build. Beyond that, subscribers reachable by identifier, but only where the recovered credential is shown to be honoured across an account boundary.
Severity range
Low to Medium where the key authenticates only to pre-auth or non-privileged endpoints, carried mostly by rotation cost. High where it authenticates to data endpoints with per-user authorisation still intact. Critical once a recovered key is shown live to return a second subscriber's record or to complete a provisioning action with no user session.
Tested against
OWASP MASVS, storage and authentication requirements, with the MASTG as the testing guide

What it is

A hardcoded API key in a mobile app, formally use of hard-coded credentials, CWE-798, is readable by anyone who installs the app, so the key is published rather than secret and whoever extracts it can call your backend directly. An Android package, the APK, decompiles into readable classes, resources and manifest entries with freely available tooling, and an iOS binary gives up the same string constants once it is decrypted on a handset the attacker owns, while bundled configuration inside the IPA comes out with no decryption at all. Hardcoded secrets turn up in build-config constants, resource XML, bundled configuration files, third-party SDK initialisation and native libraries. Testing the same build reaches the second half of this class, cleartext storage of sensitive information, CWE-312, where the app writes tokens and customer records into app storage that may leave the device through a platform backup path.

The expensive part is not the extraction, it is what the credential turns out to carry. A key compiled into an app is issued to the application rather than to a person, so the question a test answers is whether your backend treats it as sole authority: whether it authenticates with no user session at all, whether it reaches subscriber data or provisioning endpoints, and whether a client-supplied subscriber identifier is honoured across an account boundary. Each of those is shown against a negative control or it is not shown, and the severity follows the answers rather than the class. Where all three hold, one recovered string reaches accounts the attacker never authenticated to, which is an authorisation defect, CWE-639, with the hardcoding supplying the credential. Plenty of app keys fail that test harmlessly, because they are scoped to pre-auth bootstrap or are accepted alongside a user session token rather than instead of one.

Rotation is costly whatever the privilege turns out to be. A static credential compiled into the artefact exists on every installed handset and in copies archived by app mirrors, so revoking it breaks every copy in the field until each one updates, which makes containment a release event gated on store review and user uptake rather than a configuration change. An app that fetches a per-install credential at first run, or reads one from remote configuration, rotates as a server-side action instead, and that contrast is the argument for building it that way. In a telecommunications setting, where the records behind a lookup endpoint are registration identity data and the actions behind it change SIMs and plans, the combination of a readable credential and a slow revocation path is why this class is treated as a top-severity finding rather than a hygiene note.

How the attack unfolds

Mechanism, not a recipe. Reproduction detail stays in client reports, because a page that hands a reader a working attack is a liability.

How does an attacker get hold of your app build?

A published application is a file anyone can obtain, from the store on a device they control, from an app mirror, or from a corporate distribution channel. Nothing at this point requires an account on your platform, interception of traffic, or access to operator infrastructure. The precondition splits in two from here.

The file alone yields Android bytecode, resources and manifest entries, and on iOS the bundle resources, embedded property lists and JavaScript bundles, since store encryption covers only the main executable. Strings inside that executable, instrumentation of the running app, and anything in the app sandbox need a handset the attacker owns.

How are hardcoded secrets extracted from an APK or an iOS binary?

What extraction costs depends on how the app is built, so the build type is named before any claim about effort. Java and Kotlin bytecode decompiles back to near-source. A React Native bundle is plain readable JavaScript.

A Flutter ahead-of-time snapshot or a heavily native build yields no near-source and moves the work into binary analysis, which is slower without being a boundary. Across all of them, identifier obfuscation renames symbols and leaves string constants resolvable, and a string-encryption layer still has to decrypt at runtime, so calling that routine under instrumentation on an owned device returns the plaintext. What the sweep looks for is a backend API key, a request-signing secret, and tokens for whichever messaging or analytics services the app initialises for itself, with each recovered key then classified as a real credential or as a key that is public by design.

What does the app leave behind on the handset?

The app sandbox is then examined after a realistic session on a handset the tester controls, using a test account. Long-lived refresh tokens, subscriber numbers, device-binding seeds and cached customer records can all end up in shared preferences, plist defaults or unencrypted local databases, and that is what this step looks for. Read on its own this proves a storage weakness and nothing about other accounts, because pulling your own sandbox after your own session carries no cross-account impact.

It becomes a finding when those artefacts are restored into a second session and still authenticate, since the stored material is then a credential rather than a cache.

When does stored data actually leave the device?

The storage weakness matters off the device only where a path exists for the artefacts to travel, and that path is platform-specific enough to be tested rather than assumed. The adb backup route needs USB debugging enabled, an unlocked device and an on-device confirmation, and it is ignored for most apps targeting Android 12, API level 31, and above. Backup to Google's cloud is not attacker-readable.

On iOS, keychain items reach another handset only from an encrypted backup, which requires the device passcode and a trust relationship, and items marked as device-only are excluded from backup entirely. The conclusion that belongs in a report therefore reads as extractable on a stock device at this target API level by this path, with the OS version, the lock state and the trust state recorded alongside it. The exposure this weakness creates is the resold or serviced handset, where cached subscriber data and still-valid tokens sit on hardware that has left its owner.

Testing is performed on devices we own or that are provided for the test, with test accounts.

Why certificate pinning does not stop a recovered app key

The recovered credential is presented to the backend from an ordinary HTTP client. Certificate pinning does not help against a recovered key, because the attacker is the calling application rather than a party in the middle, so pinning never enters the picture. Throttling is more interesting, and it is measured rather than dismissed: a limit keyed to a device identifier or an app-instance header is attacker-supplied and therefore attacker-chosen, while a quota keyed to the source address or to the credential itself is where attempts at volume actually die.

What a report carries is which dimension the throttle is keyed on, the quota the recovered credential has, and the request ceiling observed. What your backend sees in the meantime is a valid credential on a well-formed request from an unfamiliar host.

How one app key could become access to another subscriber's account

This is the step the impact rests on, and it is three separate demonstrations rather than an assumption. First, the key authenticates with no user session, shown against a negative control: the same request succeeds with the key, and fails both with the key removed and with one character of it altered. Second, a read endpoint returns a record belonging to a second consenting test subscriber when the identifier is changed, with the field values verified from that subscriber's own side out of band.

Third, a state-changing endpoint completes without the customer-facing one-time code, which holds only where the step-up is enforced in the app rather than at the provisioning service. Until each one is shown, the honest statement is that the privilege carried by the recovered credential is not yet established. The demonstration stays bounded to two consenting test accounts and the smallest number of requests that shows an identifier being honoured across an account boundary.

Retrieval across a number range is the inferred consequence of that result, bounded by the measured quota, and is not performed.

Why you cannot simply rotate a key that already shipped

Where the credential is static and compiled into the artefact, it is live on every installed device and in copies archived by app mirrors, so revoking it breaks each one until it updates. Containment then runs through a store release and user uptake, which is the pressure that keeps a key known to have leaked in service. Where the app instead fetches a per-install credential at first run or reads one from remote configuration, rotation is a server-side action and the release train is irrelevant, which is the difference the fix aims at.

Attribution is the other half of the problem: extraction leaves no trace on your side, and the requests that follow carry a genuine credential and sit in the same logs as the app's own traffic, so scoping any exposure means analysing request patterns rather than searching for an unauthorised token.

Business impact

Where a recovered credential reaches a subscriber lookup endpoint and a client-supplied identifier is honoured across an account boundary, the asset at risk is the identity set operators are required to collect at registration wherever SIM registration rules apply: name, address, identity document reference, plan and billing state, usage history and device identifiers. That set is usable straight away for preparing SIM swaps, for social engineering your own retail and call-centre staff with details only a real subscriber should know, and for resale. How many records such an attacker reaches is a question of the per-credential quota and the volume anomaly detection actually in place, which is why the quota is measured during testing instead of assumed, and notification obligations start with a subscriber count that is hard to produce in the first week.

A mobile operator is the identity provider that much consumer banking, wallet and email recovery quietly depends on, so a path that moves a number to a new SIM is a path to intercepting one-time codes for accounts held elsewhere. Losses then surface across banks, wallets and your own business in a liability argument that takes months to settle. Direct revenue damage is simpler to total: bundles and add-ons activated without payment, roaming or premium services charged to subscriber accounts that have to be credited back, and, where a messaging gateway token ships in the same bundle, outbound traffic sent at your cost and under your sender identity. On top of that sit fraud operations, credit write-offs and the cost of the rotation programme itself.

For a licensed operator, subscriber data handling and SIM issuance are licence conditions and not privacy matters alone, so a SIM or plan change path that is reachable against arbitrary numbers is a licence-condition matter as well, with a notification clock attached under whichever data protection regime covers the subscriber base. The operational response is awkward in a specific way: the fix is an app release on both platforms plus a minimum-version gate, every integration sharing the credential has to be re-issued in step, and retail and call-centre channels absorb the subscriber contact while that happens. Scoping what was actually reached is its own cost, because requests made with a recovered credential are well-formed and carry a genuine key, so the work is behavioural analysis of request shape rather than a log query.

The storage half of the class carries a smaller bill of its own. Demonstrated against a tester's own sandbox it is a hygiene finding with no cross-account impact. It becomes expensive at the point where handsets leave their owners, because a resale, repair or warranty channel moves cached subscriber records and still-valid tokens into other hands at whatever rate your user base turns over devices, and each of those sessions has to be invalidated server-side before the data stops being live.

How it is found

What a tester looks for
SignalHow it is confirmed
Credential material inside the shipped artefactWe decompile the store build and sweep its classes, resources, manifest entries, bundled configuration and native libraries for known key formats and high-entropy strings. A candidate becomes a finding only on a differential test from a clean host with no app installed: the request succeeds with the key, and fails both with the key removed and with one character of it altered. We also record whether the key is the same for every install or fetched per install at first run.
Keys that are public by designSeveral key types shipped in mobile apps are published deliberately and are not findings on their own, among them Firebase mobile API keys, Maps keys, Sentry DSNs, search-only Algolia keys and OAuth client identifiers. Every recovered key gets an explicit determination, documented key by key, before anything is written up as a leak.
Secrets that are derived, decrypted or assembled at runtimeStatic sweeps miss anything the app reconstructs in memory, so we hook configuration and cryptographic accessors with runtime instrumentation on a test device and record the resolved values at the point of use. This also settles whether an obfuscation layer is a delay or a boundary.
Whether the secret is already public or already revokedA key published in vendor documentation, or one the backend stopped honouring two releases ago, is not a finding. We check each recovered key against public sources and against the issuing service for current validity before it goes any further.
The privilege the recovered credential actually carriesWe walk the API surface with the key alone and no user session, then compare that against what the app's own screens use. A gap there is a lead and not a confirmation, because a privileged-looking endpoint may return only the caller's own records, masked fields or stub data. Confirmation is a record belonging to a second consenting test subscriber, with field values verified from that subscriber's own side out of band, plus one state change observed from inside that second account.
What the app leaves on the device after a real sessionWe log in a test account, exercise the main flows, then pull and inspect the app sandbox: shared preferences, local databases, caches, plist defaults and keychain or keystore entries. Confirmation comes from restoring those artefacts into a second session and showing that they still authenticate.
Whether those artefacts can leave the handsetWe review backup flags and backup rules, keychain accessibility classes and device-only attributes, and exported component and deep-link surfaces, then attempt extraction through the platform backup path on a device that is neither rooted nor jailbroken. The result is recorded with the OS version, the app's target API level and whether the device was unlocked and trusted, so the conclusion reads as extractable by this path at this target level rather than as a blanket claim.
The quota the recovered credential carriesBefore any statement about volume, we measure which dimension the throttle is keyed on, whether that is the source address, the credential, the account or a client-asserted header, and the request ceiling the credential reaches. That number is what separates a confirmed authorisation failure from a claim about bulk exposure.

How it is fixed

Controls that hold
ControlWhat makes it hold
No client-side secret in the app that carries privilegeThe app holds only a short-lived, per-user credential issued after authentication, and every endpoint reachable before that credential exists, meaning login, registration and configuration, is safe to expose unauthenticated, throttled, attested and unable to reach subscriber data. It holds only when secret scanning runs against the signed release artefact and fails the pipeline, and when every key that has to stay in the app carries a documented privilege review, because scanning matches known formats and misses custom formats, constants inside native libraries and secrets assembled at runtime.
Privileged calls behind a server-side brokerSubscriber lookup, provisioning and SIM operations are reached only through your own backend, which derives the subject from the server-validated session and ignores any client-supplied target identifier, on every method and every variant including single read, bulk, export, search and legacy or internal paths. The control is real when the broker enforces that authorisation decision, and restricting the network path to the broker is defence in depth rather than the control itself.
Credentials bound to a user, a device and a short lifetimePlatform attestation establishes that a request comes from a genuine app instance, with a fresh server-generated nonce, a short verdict validity window and replay rejection, so a verdict cannot be relayed from a different device. The token issued after login is short-lived, and its refresh credential is bound to a hardware-backed, non-exportable device key. Devices that cannot attest or cannot hold such a key need a stated policy: reduced privileges with no access to subscriber lookup or provisioning, because a silent fallback to a static path becomes the route an attacker selects.
Nothing stored locally that is useful off the deviceSubscriber data is not cached on the handset at all, and where a cache cannot be avoided it is encrypted under a hardware-backed key, bounded in time, and cleared on logout and on session expiry, since data marked device-only is still data on a resold handset. User-authentication binding is applied to the refresh credential and to high-risk operations, with a stated fallback elsewhere so that silent token refresh and background sync keep working and the control does not get removed after the report is filed. Verified by pulling the sandbox after a real session rather than by code review alone.
Independent step-up on SIM and plan changesHigh-risk provisioning actions require a factor the backend owns, such as out-of-band confirmation, a cooling-off period or an identity check in a staffed channel, enforced at the provisioning service and not in the app, and one-time code issuance and verification are not reachable with the same credential that reaches the lookup. Per-account and per-credential rate limits with alerting are what leave a recovered key unable to change a SIM.
Rotation as an operational action rather than a releaseCredentials are issued at runtime and versioned per platform, with a server-side kill switch for each credential and a server-enforced minimum app version, so revocation does not wait on store review and user uptake. That is what turns the next leaked credential from an emergency release into a configuration change.
Handling a shipped credential as already extractedUse of the credential outside expected app patterns is alerted on, covering unfamiliar source ranges, missing attestation and the shape of the access pattern. Where any sign of abuse appears, the credential is rotated, historical logs are reviewed for earlier use of it, and the subscriber and regulatory notification path is run on what that review finds. A credential that ships inside a published app is in users' hands by definition, so the incident path belongs in the fix rather than in a separate exercise.

Questions

Can someone decompile our Android app and find the API key?

Yes. Unpacking a published Android package and searching it is a mechanical step measured in minutes, not a skilled one, and string constants survive decompilation intact. Keys also sit in build-config fields, resource XML, bundled JSON and native libraries, so a sweep that reads only the recovered Java source misses them. The question that decides severity is not whether the key can be read but what it authenticates to, and that is answered endpoint by endpoint.

Does ProGuard or obfuscation stop secrets being extracted from an APK?

No. Obfuscation renames classes and methods, which slows a person reading the code, but it does not remove string constants, and anything the app can decrypt at runtime an attacker can decrypt by calling the same routine under instrumentation on a device they own. Obfuscation raises the time cost of extraction. It is not a place to keep a credential.

Where should a mobile app keep secrets if not in the code?

The app should not hold a shared secret at all. It should hold a short-lived, per-user credential issued by your backend after the user authenticates, kept in the platform keystore or keychain, bound to the device and non-exportable. Anything that must stay secret belongs behind an endpoint the app calls, with the authorisation decision made on the server from the session rather than from a parameter the app supplies.

We shipped an API key inside a released app. Can we just rotate it?

Rotation is necessary, but for a static compiled key it is a release problem rather than a configuration change. The old key is live on every installed handset, so revoking it breaks those copies until each device updates, and that is the pressure that keeps a key known to have leaked in service. Move the privileged calls behind your backend, ship an app that no longer needs the key, enforce a minimum version, then revoke. An app that already fetches its credential at first run can skip that sequence and rotate server-side.

Our app uses certificate pinning. Does that protect the API key inside it?

No, because pinning defends the channel and this attack does not use the channel. Once the credential has been read out of the binary or the app sandbox, the attacker calls your API directly as the calling application, and pinning never enters the picture. Pinning is still worth having against interception, and it does nothing to make a secret held inside the app safe.

Can someone read our app's Keystore or keychain items from a stolen or second-hand phone?

It depends on how each item was stored and what state the handset is in. Keystore and keychain entries are protected against another app reading them, and marking an item as device-only keeps it out of backups, but an unlocked handset, a known passcode, an encrypted backup taken from the device, or an item stored without user-authentication binding can each put the material back in reach. Cached customer data is the larger exposure on a resold handset, which is why subscriber records should not be written to the device at all.

Would secret scanning in our CI pipeline have caught a key like this?

Only if it ran against the signed build rather than the repository, and even then only for key formats it recognises. Credentials injected by build configuration, placed in resource files, shipped inside third-party SDKs or assembled at runtime pass repository scanning cleanly, and custom-format keys and constants inside native libraries pass artefact scanning too. Scanning belongs in the pipeline as a gate on the release artefact, paired with a privilege review of every key that has to stay in the app.

Test for this on your stack

Describe the platform and the deadline. A fixed quote follows a short scoping call.