Why embedding secrets in mobile apps is not a good idea

Why embedding secrets in mobile apps is not a good idea

This is a somewhat complicated topic to cover, but I'll try to go into detail on why, generally, this is not a good idea and you should avoid embedding secrets in your mobile applications as much as possible.

tl;dr Anyone motivated enough will get these secrets and impersonate your mobile app. To err on the side of caution, just don't do it.

Now the long version. Before I start, let me define what am I referring to when I say secrets.

What are secrets on mobile apps?

Secrets are pieces of information that shouldn't be known by a 3rd party not involved in the original communication. Their most common forms are symmetric keys (a.k.a. shared keys), asymmetric keys (a.k.a. secret/private keys), passwords and PINs.

For example a developer may want to encrypt their app's local database using SQLCipher. To perform this operation the application needs to provide a password, usually a long alphanumeric sequence. Or a library wants to send some encrypted message to their backend and uses a shared key as one of the parameters for an AES-256-GCM or ChaCha20-Poly1305 method. Or an application wants to send a signed message to its server using an RSA private key.

All of these are very, very common examples among currently active mobile applications and/or their 3rd party frameworks.

What are (usually) not secrets on mobile apps?

Most of the 3rd party frameworks that provide a cloud service will give developers some credentials to log in on their websites and manage their developer account. As part of the credentials, these services also provide what's commonly known as an API key or an API Token. Most of these API keys are not secrets since they only work as identifiers. These services want to know who is accessing their APIs and they'd usually ask you to add it to the mobile app's config file, (like Info.plist). But just to be sure, you should confirm with your provider that their API keys are allowed to be public.

Why is a bad idea to embed secrets on mobile apps?

Now that we are on the same page on what I call secrets, let's get back to why you shouldn't embed secrets on your mobile apps. The whole point of secrets is to protect something, usually the users' data stored locally or the data sent to the backend.

The real problem is that any secret embedded in a mobile app can be retrieved by reverse engineering the app. It can be obfuscated or even encrypted (with another secret), but at the end of the day, the secret needs to be used by the app and attackers can use the app's runtime to de-obfuscate or decrypt the secret. But this is generally not necessary, as most apps just have the secrets in plaintext.

Let's clarify the point with a real life example

This app has a private key embedded within its bundle:

And it has a method to load that RSA secret key loadPrivateKeyFromBundledP12:password:error:

The .p12 certificate is password protected, great! However, the password is embedded within the binary.

This app was using the private RSA key to request a token from the backend that then the app could use as a bearer token for their API authentication scheme. My guess is that their intention was to seamlessly authenticate the API requests and prevent others from querying their APIs because "none would have an initial signed API request".

The problem is that anyone skilled enough to reverse engineer their application, has access to their private RSA key and therefore can make API requests to their backend system, pretending to be their app. This affects any other treat model(s) based on this trust scheme.

The same is true for an embedded password or symmetric encryption key to protect a local database or data sent to a backend (on top of using TLS). The issue here is that every single instance of your mobile app uses the same key/password to protect a local database or encrypt the data sent to the backend. If an attacker gets a hold of an encrypted database (how an attacker could get a local database is beyond the scope of this post. But for example, imagine a cloud backup is leaked) or the attacker wants to decrypt the data sent to the backend all they need to do is capture the traffic and decrypted with the retrieved key.

Alternatives

I was also asked for some alternatives. This is hard because most of these implementations are very particular and unique to your needs. But here are some ideas on how to use and generate secrets on your mobile apps.

Diffie–Hellman

You can use Diffie–Hellman to create a shared secret that only your mobile app and your backend should have. With this scheme your app generates a public and private keys, let's call them client_pubkey and client_privkey and your server also generates a public and private keys, let's call them server_pubkey and server_privkey then your mobile app sends a message to the server, let's call it m1, it contains its public key and a random piece of data, let's call it payload:

m1 := [client_pubkey, payload]

The server then takes your app's public key and mixes it with its private key. This operation generates a secret, let's called shared_secret. The server then encrypts the payload with the shared_secret using a symmetric encryption. (for example AES-128-CBC) and sends a message back to the app with its public key:

shared_secret := ECDH(`server_privkey`, `client_pubkey`)

nonce := // generate a random nonce
ciphertext := AES(shared_secret, nonce, random)

Except, no symmetric encryption is complete without an integrity tag (a.k.a. authenticated encryption), so we need to sign the ciphertext. (for example using HMAC), so let's stretch the shared_secret (using for example PBKDF2 or scrypt) to have an encryption key and an integrity key:

long_key := STRETCH_FUNC(shared_secret)

encryption_key := long_key.subkey(0, key_size)
integrity_key := long_key.subkey(key_size, long_key.length)

nonce := // generate a random nonce
ciphertext := AES_ENCRYPT(encryption_key, nonce, random)

signature := HMAC(ciphertext || nonce, integrity_key)

m2 := server_pubkey || ciphertext || nonce || signature

# `||` denotes concatenation

The server needs to send its public key to the app and the encrypted payload. The app then verifies the message m2 by using the server's public key to generate the shared_secret and decrypt the ciphertext.

payload = // the original random data sent to the server

server_pubkey := GET_SERVER_PUBKEY(m2)
ciphertext := GET_CIPHERTEXT(m2)
nonce := GET_NONCE(m2)
signature := GET_SIGNATURE(m2)

shared_secret := ECDH(client_privkey, server_pubkey)

long_key := STRETCH_FUNC(shared_secret)

encryption_key := long_key.subkey(0, key_size)
integrity_key := long_key.subkey(key_size, long_key.length)

if (HMAC(ciphertext || nonce, integrity_key) != signature)
  throw 'Bad signature'

plaintext := AES_DECRYPT(encryption_key, nonce, ciphertext)

if (plaintext != payload)
  throw 'Not the right payload'

# If it reaches here, the transaction is complete
 

Note: this is just an example of an initial scheme and you should design your own with your app sec, cloud sec and infra sec teams. The main idea is that you don't need to hard-code any secrets and yet you are able to still use secrets to protect data.

Local symmetric encryption keys or passwords

Once again, the problem with embedding these is that every instance of your mobile app uses the same key/password. A simple solution is to generate a random new key for every instance. This way even if an attacker manages to get your users' cloud backups, they'd have to find the unique key/password for every encrypted resource.

Conclusions

An attacker that's motivated will have access to any secrets you embed in your mobile app and therefore will compromise any trust scheme(s) you designed based on these secrets. You are better off not embedding/hard-coding secrets on your mobile apps. They will not accomplish what most of the developers are trying to use them for and just gives them a false sense of security. Similarly as in the "web world" where it's advised to not trust any inputs, don't trust your mobile app APIs requests.

This post was inspired by a simple tweet I posted a few days ago:

There were some interesting replies like "wouldn't use HTTPS be enough to share a secret"? This is a great point. Even though HTTPS has its issues depending on the version being used, algorithms, etc. it's usually robust enough to protect secrets, but that's not the treat model I'm describing.

It's not a matter of sharing secrets between a mobile app and a backend (or even another mobile app). The problem is using those secrets to protect data sent to a backend or stored locally.

In other words, you should treat any piece of information you embed in your mobile app as a compromised asset. You cannot trust any of these compromised assets and you should create your threat models using this assumption.

As always, these are my personal views and all of this I've learned while reverse engineering a lot of mobile apps, however if you see an error or something is not accurate please let me know @ivRodriguezCA.

p.s. The RSA private key in the hero image was generated specifically for this post. It's not a production key and it's not used by anyone ;)