Mobile App Security: Certificate Pinning, Obfuscation, and Secure Storage

A third-party penetration test on a fintech client's app found, within the first hour, an API key hardcoded in a string resource file and no certificate pinning on the connection to their payment backend, meaning a proxy tool like mitmproxy or Charles could intercept and inspect (or in theory manipulate) traffic on a rooted or jailbroken device with a user-installed CA certificate. These findings weren't exotic — they're the two most common issues we now check for on every security-sensitive client project from day one rather than waiting for a pen test to surface them.
For certificate pinning, we pin against the public key hash of the certificate rather than the full certificate itself, since full-certificate pinning breaks the app on every certificate renewal unless the pinned value is updated in lockstep with the backend team's renewal schedule — we've seen apps go dark for users after a cert rotation because pinning wasn't updated in time. We implement this with URLSession's pinning delegate on iOS and OkHttp's CertificatePinner on Android, and we always pin at least two keys (current and a backup) so a planned rotation doesn't require an emergency app release.
Secrets never go in source code or resource files on any project now — API keys and tokens that must live on the client are stored in the iOS Keychain or Android Keystore, and anything that can instead live server-side (most third-party API keys can be proxied through our own backend) gets moved there entirely, removing the client-side secret exposure surface altogether rather than just obscuring it.
Code obfuscation matters more on Android given how straightforward APK decompilation is compared to iOS. We run R8 with aggressive obfuscation and shrinking enabled on every release build, and for clients with genuinely sensitive business logic (a pricing-calculation client, for instance) we've added additional string encryption for hardcoded strings that would otherwise sit in plaintext in the decompiled output, since R8's default obfuscation only renames symbols and doesn't touch string literals.
Secure local storage for sensitive data at rest — cached user data, auth tokens, locally stored documents — uses platform-provided encrypted storage (EncryptedSharedPreferences or Jetpack Security's encrypted file APIs on Android, Keychain or a file protected with NSFileProtectionComplete on iOS) rather than plain SQLite or SharedPreferences, and for genuinely high-sensitivity data we've built additional application-layer encryption on top using a key derived from a value stored in the platform's secure enclave, so even a compromised device backup doesn't expose readable data.
None of these are individually novel, but we now run through this list as a standing checklist at the architecture stage of every project handling payment, health, or identity data, rather than treating security review as a pre-launch audit — retrofitting certificate pinning and secure storage after a feature is built and shipped costs meaningfully more than building it in from the start, and the pen test findings we still see most often are the ones a checklist at kickoff would have caught.

