Implementing API key protection in mobile applications
strings app.apk | grep -i "key\|secret\|token" — first 10 lines of output already yields something interesting in most apps without protection. Google Maps API Key in AndroidManifest.xml, Firebase API Key in google-services.json, Stripe Publishable Key in code — all readable from unpacked APK without single line of reverse-engineering.
Why you can't store keys in code
Any key in string resources, class constants or app config files — public key. APK and IPA decompile. Even obfuscation only complicates search, doesn't make it impossible.
Common argument: "Firebase API Key is public, can be exposed". Technically true for apiKey in Firebase config — needed only for project identification, access controlled by Firebase Rules. But Google Maps API Key, Stripe Secret Key, keys to your own backend — different story. Maps Key leak means others' requests on your account.
Correct secret storage on device
If key must be on device (e.g. server issues token after auth) — only Keychain (iOS) or Android Keystore.
On Android:
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
// create encryption key tied to KeyStore
val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGen.init(
KeyGenParameterSpec.Builder("my_key_alias",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
)
// encrypt token, save encrypted blob to EncryptedSharedPreferences
EncryptedSharedPreferences from androidx.security:security-crypto — convenient wrapper doing this under hood. No need to write KeyStore code manually.
On iOS Keychain Services via SecItemAdd / SecItemCopyMatching. In Swift conveniently via KeychainAccess or SwiftKeychainWrapper. Attribute kSecAttrAccessible = kSecAttrAccessibleWhenUnlockedThisDeviceOnly — data accessible only when device unlocked, don't migrate on iCloud backup.
Keys that should not be on client
API keys to external services (payment gateways, SMS providers, AI APIs) — on server. Period. Client makes request to own backend, backend makes request to Stripe/Twilio/OpenAI with its key. Client never learns this key.
Pattern for limited-use keys: client authenticates, server issues short-lived token (JWT or HMAC-signed nonce) with specific permissions. Client uses token for direct requests to service (e.g. file upload directly to S3 via presigned URL). Main key — never leaves server.
NDK and native storage
If string must be in app and can't request from server — native code. JNI function returns key assembled from parts:
JNIEXPORT jstring JNICALL
Java_com_example_NativeKeys_getApiKey(JNIEnv *env, jobject obj) {
// key split, parts in different places
const char part1[] = {0x41, 0x42, 0x43, 0x00};
const char part2[] = {0x44, 0x45, 0x46, 0x00};
// assemble + XOR decrypt
// ...
}
This is security through obscurity, not true protection. But native code is harder to hook with automatic tools, entry threshold grows.
Build-time protection
local.properties — file outside repo, holds build variables:
MAPS_API_KEY=AIzaSy...
In build.gradle:
manifestPlaceholders = [mapsApiKey: properties["MAPS_API_KEY"] ?: ""]
In AndroidManifest:
<meta-data android:name="com.google.android.geo.API_KEY" android:value="${mapsApiKey}"/>
Key doesn't land in repo, but lands in APK — still readable from manifest. For Maps Key acceptable with proper restrictions in Google Cloud Console (restrict by Android app package name + SHA-1). For Secret Keys — no.
Timeline for implementing full scheme: audit current key position, migrate to Keystore/Keychain, move server keys to backend, configure restrictions in Google Cloud/App Store Connect — 2–3 days.







