All Articles
CategorySecurity
Reading Time
15 min read
Published
2025-10-16
Word Count
3,636words

Grab a coffee — this one is a deep dive!

Mobile App Secret Management: Where Do API Keys Go?

Summary

Where should an API key go in a mobile app? I cover secret management across build time, runtime, and server layers — from decompile risk to SHA pinning and OIDC in CI/CD.

  • A mobile binary can be decompiled; never embed an API key in source code.
  • Only hardware-bound keys (signing, biometric) can stay in the Android Keystore or Apple Keychain.
  • In CI/CD, pin actions to a full commit SHA, restrict GITHUB_TOKEN permission, and use OIDC for cloud credentials.
  • Invalidate a leaked key immediately — GitHub's secret scanning covers the entire Git history.
Mobile App Secret Management: Where Do API Keys Go?

An API key embedded in a mobile app's source code becomes public the moment it ships to the App Store or Play Store — because the package you distribute is a file that lands on the user's device and can be decompiled at will. This post gives a clear answer to the question of secret management for API keys in mobile apps, using a three-layer framework (build time, runtime, server): which key can stay on the device, which one must never stay there, and how you guarantee this in your CI/CD pipeline.

💡 Pro Tip: Aim to not need a key at all, rather than to "hide" it — a secret that never exists on the client side can't be decompiled; the real win isn't obfuscation, it's architecture.

Table of Contents

Threat Model: How Easy Is It to Extract a Key From a Binary?

Android's official security documentation doesn't mince words: if a compiled app has an API key embedded in its source code resources, "it is possible for an attacker to decompile your app and discover this resource" (developer.android.com/privacy-and-security/security-tips). In practice this means a mobile binary lives in a fundamentally different trust boundary than server code. Users can't physically reach your server's code; but every installed package is directly exposed to a disassembler or decompiler — anyone who opens the APK can inspect its string tables and, if not obfuscated, its source structures.

iOS is no different: the IPA file is also an archive, and every App Store package can be opened and inspected. The only difference across platforms is how hard decompiling is — an ease difference, not a security guarantee.

A common trap here is the assumption that moving the key to native (C/C++) code makes it invisible. The same Android document notes that native code is more prone to memory corruption bugs (like buffer overflows) than Java/Kotlin. So moving to native doesn't eliminate the decompile risk; it adds a different bug class — memory safety — on top. A key kept as a string in a native library can still be extracted with basic binary analysis; the real fix is to never put the key on the client at all. Obfuscation (ProGuard/R8) can slow this down but doesn't make it impossible — it's one layer of defense, not a strategy on its own.

This threat model's conclusion is simple: on mobile there's no "hidden" category, only "present" and "not present." If a key exists anywhere in the binary — in a source file, a native library, even an encrypted blob whose decryption key is also inside the binary — it's just a matter of time for a motivated attacker. The next section splits, in a three-layer framework, which keys should never fall into this category at all.

Three Layers: Build Time, Runtime, Server

Thinking about secret management in three separate layers clarifies which tool to use where. Each layer has its own threat model, its own tooling, and its own "never" rule:

Layer
What's stored
Typical mechanism
Can it stay as plaintext on mobile
Build time
Environment variables injected at build time
Gradle/Xcode build config, CI secret injection
No — only transient, gets baked into the build output
Runtime
Keys generated/encrypted on the device
Android Keystore, Apple Keychain
Yes — hardware-backed, never leaves the device
Server
Fixed/shared keys of third-party APIs
Backend proxy, short-lived token
No — must never be sent to the client at all

At build time, Android's documentation gives a clear rule: "Never commit API keys to your source code repository." In practice this means moving keys into .gradle files or CI environment variables and injecting them at build time with tools like secrets-gradle-plugin. Which tool you pick depends on your project — what matters is that the key never appears as plaintext in git log. A local.properties or .xcconfig file added to .gitignore is the simplest way to do this.

At runtime, both Android and Apple offer a hardware-backed store encrypted at the OS level. Apple's Keychain Services documentation defines this clearly: the Keychain "securely stores small chunks of data on behalf of the user" — passwords, cryptographic keys/certificates, short notes (developer.apple.com/documentation/security/keychain-services). The Android equivalent, Android Keystore, keeps the key outside the app's process — the app can only say "sign/encrypt with this key," it can't read the key itself.

The server layer holds the fixed/shared keys of third-party services (payment providers, map APIs, analytics, push). These keys serve the entire user base, not one app instance, so they must never reach the client — they stay behind a backend proxy. The mobile app should reach that service through its own backend, via an endpoint protected by its own authentication, never directly.

kotlin
1// app/build.gradle.kts — injection from local.properties via secrets-gradle-plugin
2plugins {
3 id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
4}
5// local.properties (not committed to git, in .gitignore):
6// MAPS_API_KEY=xxxxx
7// After build, accessible as BuildConfig.MAPS_API_KEY — no literal in source code

Which Key Can Stay on Mobile, and Which One Never Can?

Two facts side by side make this distinction clear: the decompile risk above, and the Keychain/Keystore's design purpose — storing hardware-bound data on the user's behalf. The practical rule:

  • Can stay on mobile: keys generated on the device that never leave the Keystore/Keychain — signing keys, local encryption keys, keys tied to biometric auth. These are already hardware-bound and "impossible to exfiltrate"; even the app can't read the raw value, only "operate with this key." In iOS Keychain and Security I detail using this API with certificate pinning.
  • Must never sit as plaintext on mobile: a third-party cloud API's shared/fixed key. Since the decompile risk is real, a key like this, if embedded literally, can sooner or later be extracted — and how many users it "serves" scales the leak's blast radius. In Mobile Backend API Security I cover reducing this risk with rate limiting and token rotation; the mobile side should never see keys of this kind, only a short-lived session token from its own backend.

There's also a middle category: long-lived, user-specific session tokens (e.g., a "remember me" refresh token). Not a third-party fixed key, but still shouldn't sit as plaintext — write it encrypted to the Keychain/Keystore, and make it revocable server-side. iOS Security Best Practices covers this session-management pattern in general.

swift
1// Writing encrypted to the Keychain — the key itself never appears in source code
2let query: [String: Any] = [
3 kSecClass as String: kSecClassGenericPassword,
4 kSecAttrAccount as String: "refresh_token",
5 kSecValueData as String: tokenData,
6 kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
7]
8SecItemAdd(query as CFDictionary, nil)
9// kSecAttrAccessibleWhenUnlockedThisDeviceOnly: can't be read while the device is locked,
10// doesn't move to backups or another device

Secret Management and Signing Keys in CI/CD

The CI/CD pipeline is the most neglected layer of mobile secret management — the key no longer lives on a developer's laptop, but in an automated environment that runs on every push and includes third-party actions. GitHub's own security documentation (docs.github.com/en/actions/reference/security/secure-use) lists three concrete measures:

  1. Pin actions to a full commit SHA. According to the documentation, this is "the only way to use an immutable release" — relying on a tag or branch name leaves the door open for that reference to later point at different code (tag hijack). Compromise of a third-party action can mean access to the secrets of every workflow that uses it.
  2. Restrict GITHUB_TOKEN permissions. Keeping the default permission read-only and only raising it for the job that needs it is listed as "good security practice" — a workflow having write permission it doesn't need increases the blast radius of a compromised action.
  3. Use OIDC instead of static secrets for cloud credentials. The workflow requests a short-lived token from the cloud provider that's valid only for that job and expires automatically; a persistent cloud credential is never stored in GitHub at all.

Putting these three measures in a single table makes clear which one reduces which risk:

Measure
Risk it reduces
Source
Pin the action to a full commit SHA
Supply-chain attack via tag/branch hijack
GitHub Docs — Security hardening
Lower GITHUB_TOKEN permission to read-only by default
Blast radius of a compromised action
GitHub Docs — Security hardening
Use OIDC for cloud credentials
Storing and leaking a persistent static secret in CI
GitHub Docs — Security hardening
yaml
1# .github/workflows/release.yml — SHA pinning + restricted permissions + OIDC
2permissions:
3 contents: read # once you write permissions in a job, everything you don't list becomes none — restate contents: read
4jobs:
5 sign-and-deploy:
6 runs-on: ubuntu-latest
7 permissions:
8 contents: read
9 id-token: write # only to request an OIDC token
10 steps:
11 - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # full commit SHA
12 - name: Cloud auth (OIDC, no static secret)
13 uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502
14 with:
15 role-to-assume: arn:aws:iam::123456789012:role/ci-deploy
16 aws-region: eu-central-1

Signing keys (the Android keystore file, the iOS distribution certificate + provisioning profile) are where these three rules matter most: keep them only as encrypted CI secrets, never print them to workflow logs (GitHub masks secrets in logs automatically, but since the value can be transformed, redaction isn't guaranteed — avoid structured data like JSON/XML/YAML as a secret), and use them, where possible, in a separate, restricted-permission job triggered only on a release tag. I walk through this on Flutter with Fastlane in Flutter CI/CD: GitHub Actions and Fastlane; for general CI/CD principles (parallel jobs, cache, release channels), see Mobile DevOps Best Practices.

Detecting a Leaked Key and a Rotation Plan

GitHub's secret scanning scans the entire Git history across all branches for known secret types — so even a key deleted in a later commit stays scannable as long as it's in history (docs.github.com/en/code-security/concepts/secret-security/secret-scanning). This invalidates the assumption of "I deleted it, so I'm safe now": anyone who runs git log -p can still see a key that's gone from HEAD but still in history. So once a leak is detected, the correct step isn't cleaning up history (usually impractical, with force-push risks) — it's invalidating the key immediately.

For credential types integrated with partner providers (e.g., cloud providers), the detection is reported directly to the provider, so revocation can start even before you notice. That's a safety net, not a substitute for your own monitoring — not every provider has this integration.

Once a leak is detected, invalidate the key from the provider's dashboard immediately and replace it — GitHub's own guidance also says to rotate the credential right away on alert. Treat rotation as an incident-response step triggered by "a leak was detected," not routine maintenance; keeping an inventory of which key belongs to which service and who owns it cuts the response delay.

Scanning the Repository for Secrets and a Pre-Commit Hook

Secret scanning's retrospective (post-push) behavior is clear above; its complement is a local scanner that runs before a commit is even created, catching known key patterns (e.g., provider-specific prefixes) and rejecting the commit.

The rule: keep secret scanning on at the repo/org level (a retrospective net), and add a local pre-commit hook on top (the first line of defense). They don't substitute for each other — one scans history, the other stops a new commit before it enters the repo. For a team, the second layer's real value is catching the mistake on the developer's own machine before it's shared — far cheaper than rotating afterward.

bash
1#!/usr/bin/env bash
2# .git/hooks/pre-commit — a simple pattern-based check example
3if git diff --cached | grep -E "AIza[0-9A-Za-z_-]{35}|sk_live_[0-9a-zA-Z]{24}"; then
4 echo "Possible API key detected — commit rejected."
5 exit 1
6fi

Checklist

  • Source code: never commit an API key; inject it from a local file added to .gitignore or from a CI secret.
  • Static/shared key: don't keep it as plaintext in the mobile binary — the decompile risk is real, obfuscation alone isn't enough.
  • Keystore/Keychain: use only for hardware-bound keys that never leave the device; also keep long-lived session tokens encrypted.
  • CI/CD: pin actions to a full commit SHA, lower GITHUB_TOKEN permission to read-only by default, use OIDC for cloud credentials.
  • Signing keys: use in a separate, narrowly-permissioned job triggered only on a release tag.
  • Leak: keep secret scanning enabled at the organization level, treat detection as an incident-response step for rotation.
  • Pre-commit: complement retrospective scanning with a local scan hook, give the team a one-line setup instruction.

GOLDEN TIP

The most valuable insight in this article

This tip holds the article's most important takeaway.

Easter Egg

You found a hidden gem!

There's a hidden detail in this section. Want to uncover it?

Reader Reward

If you're going to apply this post's seven-item checklist to your own project, prioritizing the order makes the job easier. The list below shows, from highest risk to lowest, which step you should take first — the "is there a literal key in the source code" check in particular is usually the one to do first and the fastest win; isolating the signing key, meanwhile, is the step that requires the most architectural change, but has the highest impact.

FAQ

Where should an API key go in a mobile app?

It depends on the key's type: keys generated on the device that never leave it — signing, local encryption, biometric-tied keys — can stay in the Android Keystore or Apple Keychain. A third-party service's shared/fixed key should never go on the mobile side; keep it behind a backend proxy, and send the device only a short-lived, narrowly-scoped token. Ask yourself: "if this key leaks, how many users does it affect?" — if the answer is "all of them," it shouldn't be on mobile.

How does someone find a key embedded in code?

Android's own documentation states it explicitly: a compiled app can be decompiled to extract values embedded in its source and resources. Obfuscation (ProGuard/R8) makes this harder, not impossible; real security comes from never putting the key on the client at all. On iOS, the IPA is likewise an openable, inspectable archive — the platform difference only changes the difficulty, not the guarantee.

How are secrets managed in CI?

In GitHub Actions, secrets are stored encrypted, injected as environment variables at runtime, and masked in logs. On top of that, pinning actions to a full commit SHA, restricting GITHUB_TOKEN permission, and using OIDC for cloud credentials are the three concrete measures GitHub's own guidance recommends. They complement each other: SHA pinning cuts supply-chain risk, permission restriction cuts blast radius, and OIDC removes the need to hold a persistent credential.

Is it safe to store a key in native code (C/C++)?

No — Android's docs note that native code is more prone to memory corruption bugs (like buffer overflows); moving to native doesn't remove the decompile risk, it adds a different bug class — memory safety. A key kept as a string in a native library can still be extracted with basic binary analysis.

Does secret scanning also scan past commits?

Yes — GitHub's secret scanning covers the entire Git history across all branches; a key deleted in a later commit can still be detected as long as it remains in history. This invalidates "I deleted it, problem solved" — the correct step is invalidating the key immediately, not erasing history.

What should my first step be when I notice a leaked key?

Invalidate the key from the provider's dashboard and replace it; then update every system that used it (app, backend, CI jobs). GitHub's own guidance also recommends immediate rotation for a leaked credential — don't rely on cleaning history or on "no one noticed."

Update (September 2026)

The body of this post was written based on the state of tools and documentation as of 2025-10-16. In September 2026, GitHub made two concrete changes; both directly overlap with this post's topic — preventing mobile/CI secret leaks:

  • September 9, 2026: GitHub added a repository ruleset rule that "blocks merging while a secret scanning alert is open on a pull request" (currently public preview, for GitHub Secret Protection/Advanced Security customers). Developers without bypass permission must resolve every alert to merge. This differs from push protection: push protection tries to catch a secret at push time, while this rule adds a blocking layer at the pull-request stage — a safety net even for secret types where push protection is disabled (e.g., generic patterns) (github.blog/changelog, 2026-09-09).
  • September 17, 2026: "Workflow execution protections" reached general availability at the GitHub Enterprise/org/repo level (previously public preview). It lets you set an allowlist for who can trigger an Actions workflow and which events can start it — actor rules cover who, event rules cover what — and GA added per-file targeting, audit insights, and REST API management. The same update disables pull_request_target by default (evaluate mode first, automatic enforcement from November 2, 2026) for public repos without an applicable event policy: since this event can run fork code with access to the base repo's secrets (the "Pwn Request" issue), the change directly targets secret-leak risk (github.blog/changelog, 2026-09-17).
  • Android's "Security tips" page was last updated 2026-09-01; its API key guidance — no committing to source, decompile risk, the native-code warning — stays consistent with this post's advice, with no conflicting change.

The common theme: GitHub is moving from a single checkpoint (push protection) to catching leaks across multiple pipeline layers (push, pull request, workflow trigger) — the same logic as this post's three-layer framework: don't rely on one line of defense, stack several.

Conclusion

Secret management in mobile apps isn't a single tool choice, it's a three-layer architectural decision: never write the key into source at build time; keep only hardware-bound keys in the Keystore/Keychain at runtime; keep shared keys entirely off the client on the server side. Because CI/CD is where these three layers intersect, measures like SHA pinning, a restricted GITHUB_TOKEN, and OIDC can't be skipped; moving the signing key to a separate, narrowly-permissioned job is the highest-impact step among them.

For using the Keychain API together with certificate pinning, see iOS Keychain and Security; for rate limiting and token rotation strategies on the backend side, see Mobile Backend API Security; for general iOS hardening steps, see iOS Security Best Practices; for setting up CI/CD with Fastlane on Flutter, see Flutter CI/CD: GitHub Actions and Fastlane; and for general CI/CD automation principles, see Mobile DevOps Best Practices.

Sources

Tags

#API Security#Secrets Management#CI/CD Security#GitHub Actions#Keychain#Android Keystore
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

Lead Mobile Engineer with 12+ years of experience. Expert in iOS, Android and cross-platform architectures with Swift, SwiftUI, Kotlin and Flutter. I build performant, user-friendly mobile apps.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

We respect your privacy. You can unsubscribe at any time.

Share