Guide · Mobile app security
Can a mobile appkeep a secret?
A mobile app runs on a device you do not control. Its package can be unpacked, its network traffic can be observed and its behaviour can be changed. That does not make every value found inside it a vulnerability—but it does mean a real server secret cannot live there. This guide explains the boundary, using Firebase as the case people most often misunderstand.
The short answer: assume the client can be inspected
When an Android or iOS app is released, every user receives a copy of its executable code and resources. The original source is not handed over neatly, but common tools can recover strings, configuration files, endpoints and a useful approximation of the program's structure. An attacker does not need to reconstruct the whole app to search for a token or replay one request.
Obfuscation is still useful: it removes friendly names and raises the cost of understanding the code. It is a speed bump, not a vault. A value the app must read can eventually be read by someone controlling the device. The durable rule is therefore simple: design the client as observable and keep authority on a system you control.
| Reasonable in the app | Keep on the server | |
|---|---|---|
| Configuration | API base URL, project ID, feature availability | Private network locations and administrative configuration |
| Identity | A short-lived token for the signed-in user | Service-account keys and signing secrets |
| Third-party services | A deliberately public, tightly restricted client key | Payment, email, AI-model and unrestricted API keys |
| Business rules | Validation that improves the interface | Authorization, prices, entitlements and fraud decisions |
What decompiling an app can actually reveal
A first pass is often mundane. Unpack the application, list its resources and search the strings. That can reveal server addresses, analytics identifiers, Firebase configuration, feature flags and credentials that were copied into a build file. The next step is observing the requests the app makes. If a privileged action depends only on a value embedded in the client, the problem is not that the decompiler was clever; the trust boundary was in the wrong place.
Runtime storage needs the same care. A token issued to a signed-in user has to reach the device, so the goal is not to make it invisible forever. Give it a narrow scope and short lifetime, store it with the platform's protected storage, transmit it over TLS and let the server verify what that user may do on every request.
Do not confuse inconvenience with protection. Splitting a key across strings, encoding it or downloading it after launch may hide it from a basic search, but the complete value must exist when it is used. These techniques can slow bulk scraping; they cannot turn a shared secret into a private one.
The Firebase API key exception—and what it does not excuse
Open a Firebase Android build and you will usually find an API key in google-services.json; an Apple app carries the equivalent configuration. This alarms many audits, but Firebase documents these keys as public by design when they are restricted to Firebase-related APIs. They identify the project. They do not authorize a user to read Firestore, Realtime Database or Cloud Storage.
The locks are Firebase Security Rules, backed by the user's authentication state. App Check adds another signal: it helps a supported backend reject traffic that did not come from an attested instance of your app. Neither control is replaced by hiding the Firebase key.
The nuance matters. The same Google Cloud project can contain other keys whose services do charge or authorize by key. A key usable with a mapping, payment, email or generative-AI API may be valuable to an attacker. Use separate keys, restrict each one to the expected API and application, set sensible quotas, and keep any service that requires a true secret behind your server.
- The key is limited to Firebase services
- Review Security Rules, Authentication and App Check.
- The key also enables another Google API
- Separate it and apply API and application restrictions.
- The app contains a service account or private key
- Revoke it, investigate its use and move the operation server-side.
- Data is readable without the intended user rights
- Treat the rules or backend authorization as the vulnerability.
Put the security decision on the server
Imagine a booking app that shows available appointments and lets a customer reserve one. The app may know the public API address and carry a token for the signed-in customer. It should not decide that an appointment is free, set its own price or call the payment provider with your private key.
Instead, the server receives the request, verifies the user, checks the current availability and price, performs the privileged action with a server-side credential and returns only the result. This example is fictional, but the boundary is the same in a customer portal, a warehouse app or a subscription product.
Client-side checks still improve the experience: disable an invalid button, catch a malformed date, explain why an action is unavailable. Repeat every security-relevant check on the server, where a modified app cannot simply skip it.
- 01 App requests User intent plus a short-lived identity token
- 02 Server verifies Identity, permission, current state and limits
- 03 Service acts The private credential never reaches the phone
- 04 Result returns Only the data the user is allowed to receive
What App Check, Play Integrity and App Attest add
Authorization answers whether this user may perform this action. App integrity answers a different question: does the request appear to come from a genuine, untampered instance of the app? Firebase App Check can use Play Integrity on Android and App Attest or DeviceCheck on Apple platforms to provide that signal to Firebase services or your own backend.
That is valuable for limiting automated abuse, copied clients and some modified-app traffic. It is not a magic proof. Devices can be unavailable or compromised, attestation services can fail, and a genuine app can still be driven by a malicious user. Roll enforcement out while monitoring, define what happens when attestation is unavailable and keep ordinary authentication, authorization, rate limits and logging in place.
Use stronger checks at valuable moments rather than turning every harmless screen into a fortress: claiming a promotion, creating an expensive AI request, downloading paid content or changing a payment destination are better candidates than reading a public catalogue.
A practical release review
Before release, inspect the same artefact users will install—not only the source repository. Search the built package for credentials and private material; list every outbound service; verify that each key is public by design or restricted to the minimum; and attempt important requests without the app's own interface.
Then test authorization with two ordinary accounts. Can one read or change the other's records by editing an identifier? Can either invoke an administrative endpoint? Are Firebase rules tested against denied cases as well as happy paths? Finally, check logs, quotas and alerts: a control you cannot observe is difficult to trust in production.
- No service-account files, signing secrets, payment secrets or private keys in the package.
- Every client key has an owner, a purpose, API restrictions and a rotation plan.
- The server rechecks identity, ownership, roles, prices and entitlements.
- Firebase Rules deny by default and have emulator tests for allowed and denied requests.
- App attestation complements—never replaces—authentication and authorization.
- Rate limits, budgets and alerts make abuse visible before the invoice does.
If you find a real secret in a released app
Treat the value as exposed. Revoke or rotate it first, using the provider's safe overlap procedure if an immediate cutover would break production. Review access logs, billing and changes made with the credential; preserve enough evidence to understand the window; and tell the relevant people if data or accounts may have been affected.
The permanent fix is architectural: put the privileged call behind a server or managed function, authorize the user there and give that runtime only the permission it needs. Shipping another app version with the same secret encoded more creatively only restarts the clock.
Sources and further reading
- Firebase: Learn about and manage API keysWhy Firebase client keys identify a project and how to restrict them.↗
- Firebase security checklistSecurity Rules, App Check, credentials and launch controls.↗
- Android: Hardcoded cryptographic secretsAndroid's guidance on secrets recoverable through reverse engineering.↗
- Android: Play Integrity APISignals for genuine app binaries and devices.↗
- Apple: Establishing your app's integrityApp Attest and server-verified assertions.↗
Questions before we start
Can someone really decompile an Android or iOS app?
They can inspect the package, recover resources and strings, approximate parts of the code and observe runtime behaviour. Obfuscation raises the effort but does not make a value used by the app permanently secret.
Is exposing a Firebase API key a security vulnerability?
Usually not by itself. Firebase client keys are designed to identify the project. Check that the key is restricted to the intended Firebase APIs, then test Security Rules, Authentication and App Check—the controls that actually protect the data and backend.
Should I hide an API key with obfuscation or encryption?
Only as an extra obstacle for a key that is already safe to distribute. If every copy of the app can decrypt a shared credential, a determined analyst can recover it. A true server secret belongs outside the app.
Does App Check stop every copied or modified client?
No. It adds a useful app-integrity signal and reduces several forms of abuse, but it needs monitoring and fallbacks and does not replace user authentication, server-side authorization, quotas or logs.