Tue 27 September 2016
Suppose your mobile app relies on a back-end server that holds sensitive data or just data that you do not want to be manipulated or copied freely. You trust your own app to do everything right, but what about bots exploiting your API or if someone steals and subverts your app?
A malicious person could write a bot
- that performs an Application Layer DDOS attack by sending many expensive requests to your server’s API
- to perform data scraping on the server through parts of the API especially where access without user authentication is permitted (e.g. the search for a product or service)
- that can steal or modify data on your back-end server, or cheat in a game
This article describes how to make a casual bot writer’s or app repackager’s life a little more difficult by protecting against the most straightforward attacks on your API from scrapers, cheaters, id-thieves, DDoSers and other nefarious cyber-denizens.
A solution for preventing bots from exploiting your API is to have the app prove its authenticity for any request to the server. This can be done by embedding a secret into your app. The secret is then used on every API request to prove that the app is genuine.
Embedding and using a secret does not prevent, for example, repackaging a modified version of your app which would still contain the secret. But how to defend against this scenario is beyond the scope of this article.
Hide a secret in your app, but hide it well
Avoid storing secrets in the clear: when hiding a secret in your app ensure that it cannot be found just by extracting all the strings in your app. This makes it far too easy to find the secret and to use it in another app or script. You can hide a secret by encoding it using any existing method or one that you invented yourself. The encoding does not need to be complicated, its purpose is to make it non-obvious what the secret is. For example you could use xor of the encoded secret and the decoding key to restore the secret to its in-the-clear form.
Example (using pseudo-code):
// Somewhere in the code byte encodedSecret = “MyEncodedSecret”; // Somewhere else in the code byte decodingKey = “MyDecodingKey”; // Somewhere else, just before using the secret byte clearSecret = decode(encodedSecret, decodingKey);
You are now ready to use the in-the-clear secret which you should throw away immediately after use.
Some people advocate never to embed a secret or key in your app because it can be reverse engineered. While this is true, a well hidden secret can make it quite difficult and time-consuming to reverse-engineer the app and to circumvent the authentication mechanism. This is by no means impossible, but it requires more specialised knowledge from an attacker as the inner workings of the app need to be analysed. (We lock our doors because of a similar rationale: locks are not impossible to break, but getting through a locked door requires more effort than walking through one that is left wide open.)
To make analysing your app more difficult and extracting the secret harder, you should use obfuscation (e.g. Proguard for Android or PPiOS-Rename for iOS) as much as is feasible, to make names of classes, constants, variables, etc. unrecognisable.
Ensure that the secret cannot be observed in-transit
Even a well-hidden secret is still exposed when it travels across the net. It could be intercepted in transit and then used in another (malicious) app or a script. To prevent that, under no circumstances use the secret directly as the API key. Instead, the secret can be used to compute a signature of the API request. This signature is then attached to the request to prove that the sending app is genuine. This way the secret is never sent through the network and so never exposed.
Signing can be performed by computing a hash of the API request (the message), using the secret as a key. The resulting signature is called a keyed-hash message authentication code (HMAC). The server can then repeat the hash computation (using the same pre-shared secret) and check that the client and server hashes match. If they do not, the API request is rejected. The signature is different for every API request and cannot be re-used, as long as no two API requests contain exactly the same data.
Example (continued from above, using pseudo-code):
// Use the secret key to generate the signature for the API request String signature = HMAC(clearSecret, message);
Now that the signature has been computed, you can attach it to the API request to prove that the app is genuine. This also ensures that any tampering with the content of the request will be detected by the server.
How much security is enough – where does it end?
Applying the above techniques to secure your app and API will result in a basic level of protection against stealing the secret and abusing your API by casual or nuisance attackers.
It can be improved by hiding the secret as well as the signature computation in a native library that is accessed through the Java Native Interface (JNI). This can make analysing the app harder still, because the reverse-engineer now also needs to have knowledge of how to analyse machine code (assembler). As always there is a trade-off: security vs. effort required to provide it.
How far to progress down this road depends on how sensitive or valuable the protected information is. For high-value or confidential data (e.g. banking, health), unfortunately, the security level achievable by these methods is insufficient. To stick with the locked door analogy: it amounts to fitting a door with a four-digit combination lock – it will slow down an attacker, but not prevent them from entering eventually – when what you really need is a vault.