Mobile

Securing API Keys in Mobile Applications

Techniques for protecting secrets in mobile binaries

Last updated 2026-01-15

The Fundamental Problem

Mobile applications are distributed as binaries that run on devices you do not control. Any secret embedded in the binary -- API keys, tokens, credentials -- can be extracted by a motivated attacker. This is not a theoretical risk; it is routine. Automated tools can extract embedded strings from APKs and IPAs in seconds.

Classifying Your Keys

Not all API keys carry the same risk. Classify your keys to determine the appropriate protection level:

Public Keys (Low Risk)

  • Supabase anon keys (designed to be public, protected by RLS)
  • Firebase web API keys (restricted by security rules)
  • Public map API keys (restricted by domain/bundle ID)

These keys are expected to be in client code. The security boundary is server-side (RLS, security rules, API restrictions).

Restricted Keys (High Risk)

  • Supabase service role key
  • Stripe secret keys
  • AI/LLM API keys (OpenAI, Anthropic)
  • OAuth client secrets
  • SMTP credentials

These keys must never be in client code. If found in a mobile binary, it is a critical vulnerability.

Architectural Solutions

The Proxy Pattern

The most effective approach is to never put restricted keys in the mobile app at all. Use a server-side proxy:

Mobile App --[anon key]--> Your Server/Edge Function --[secret key]--> External API
// Mobile app calls your proxy
const response = await fetch('https://your-project.supabase.co/functions/v1/ai-proxy', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${session.access_token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ prompt: userPrompt }),
});

// Edge Function holds the real API key
Deno.serve(async (req) => {
  // Verify user, check credits, rate limit...

  const aiResponse = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ model: 'gpt-4o', messages: [...] }),
  });

  return new Response(aiResponse.body, { headers: corsHeaders });
});

Short-Lived Tokens

If the mobile app needs direct access to an external service, request short-lived tokens from your server:

// Server generates a short-lived, scoped token
app.post('/api/upload-token', async (req, res) => {
  const user = await verifyUser(req);
  const token = await generateScopedToken({
    userId: user.id,
    bucket: 'user-uploads',
    path: `${user.id}/*`,
    expiresIn: 300, // 5 minutes
    permissions: ['write'],
  });
  res.json({ token });
});

// Mobile app uses the short-lived token
const { token } = await api.getUploadToken();
await uploadFile(token, file);

Platform-Specific Storage

Android: EncryptedSharedPreferences

For keys that must be stored on device (e.g., user session tokens):

val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

val prefs = EncryptedSharedPreferences.create(
    context,
    "secure_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

prefs.edit().putString("session_token", token).apply()

iOS: Keychain Services

func storeInKeychain(key: String, value: String) throws {
    let data = value.data(using: .utf8)!
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecValueData as String: data,
        kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
    ]
    SecItemDelete(query as CFDictionary) // Remove existing
    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        throw KeychainError.saveFailed(status)
    }
}

What NOT To Do

1. String Encoding / Base64

// BAD: This is trivially reversible
val key = String(Base64.decode("c2tfbGl2ZV9teV9zZWNyZXRfa2V5", Base64.DEFAULT))

Base64, XOR, ROT13, or any simple encoding is not protection. Automated tools decode these instantly.

2. Splitting Keys Across Files

// BAD: Barely slows down an attacker
val part1 = "sk_live_"
val part2 = "my_secret"
val part3 = "_key"
val key = part1 + part2 + part3

3. Storing in BuildConfig

// BAD: Ends up in plaintext in the APK
buildConfigField "String", "API_KEY", "\"sk_live_mykey\""

BuildConfig values are compiled into the APK as string constants and are trivially extractable.

4. NDK / Native Code

Storing keys in C/C++ native libraries via the NDK makes extraction slightly harder but far from impossible. Tools like strings, IDA Pro, and Ghidra can extract strings from shared libraries.

Google Play and Apple API Key Restrictions

For Google Cloud API keys used in mobile apps, apply restrictions:

  • Application restrictions: Restrict to your app's package name and signing certificate
  • API restrictions: Limit to only the specific APIs the key needs

For Apple, use App Attest and DeviceCheck to validate requests come from genuine app installations.

Automated Key Detection

AuditYour.app scans your APK or IPA files to detect embedded API keys, including Supabase, Firebase, Stripe, OpenAI, and other common service keys. The scan identifies the key type, assesses the risk level, and provides specific remediation steps for each finding.

Scan your app for this vulnerability

AuditYourApp automatically detects security misconfigurations in Supabase and Firebase projects. Get actionable remediation in minutes.

Run Free Scan