Securing Communications on Android | How To
With all the recent data breaches, privacy has become an important topic. Almost every app communicates over the network, so it’s important to consider the security of user information. In this post, you’ll learn the current best practices for securing the communications of your Android app.
Use HTTPS
As you are developing your app, it’s best practice to limit your network requests to ones that are essential. For the essential ones, make sure that they’re made over HTTPS instead of HTTP. HTTPS is a protocol that encrypts traffic so that it can’t easily be intercepted by eavesdroppers. The good thing about Android is that migrating is as simple as changing the URL from http to https.
URL url = new URL("https://example.com"); HttpsURLConnection httpsURLConnection = (HttpsURLConnection)url.openConnection();httpsURLConnection.connect();
In fact, Android N and higher versions can enforce HTTPS using Android’s Network Security Configuration.
In Android Studio, select the app/res/xml directory for your project. Create the xml directory if it doesn’t already exist. Select it and click File > New File. Call it network_security_config.xml. The format for the file is as follows:
example.com
To tell Android to use this file, add the name of the file to the application tag in the AndroidManifest.xml file:
Update Crypto Providers
The HTTPS protocol has been exploited several times over the years. When security researchers report vulnerabilities, the defects are often patched. Applying the patches ensures that your app's network connections are using the most updated industry standard protocols. The most recent versions of the protocols contain fewer weaknesses than the previous ones.
To update the crypto providers, you will need to include Google Play Services. In your module file of build.gradle, add the following line to the dependencies section:
implementation 'com.google.android.gms:play-services-safetynet:15.0.1'The SafetyNet services API has many more features, including the Safe Browsing API that checks URLs to see if they have been marked as
a known threat, and a reCAPTCHA API to protect your app from spammers and other malicious traffic.After you sync Gradle, you can call the
ProviderInstaller
'sinstallIfNeededAsync
method:public class MainActivity extends Activity implements ProviderInstaller.ProviderInstallListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ProviderInstaller.installIfNeededAsync(this, this); } }The
onProviderInstalled()
method is called when the provider is successfully updated, or already up to date. Otherwise,onProviderInstallFailed(int errorCode, Intent recoveryIntent)
is called.Certificate and Public Key Pinning
When you make an HTTPS connection to a server, a digital certificate is presented by the server and validated by Android to make sure the connection is secure. The certificate may be signed with a certificate from an intermediate certificate authority. This certificate used by the intermediate authority might in turn be signed by another intermediate authority, and so on, which is trustworthy as long as the last certificate is signed by a root certificate authority that is already trusted by Android OS.
If any of the certificates in the chain of trust are not valid, then the connection is not secure. While this is a good system, it's not foolproof. It's possible for an attacker to instruct Android OS to accept custom certificates. Interception proxies can possess a certificate that is trusted, and if the device is controlled by a company, the company may have configured the device to accept its own certificate. These scenarios allow for a “man in the middle” attack, allowing the HTTPS traffic to be decrypted and read.
Certificate pinning comes to the rescue by checking the server's certificate that is presented against a copy of the expected certificate. This prevents connections from being made when the certificate is different from the expected one.
In order to implement pinning on Android N and higher, you need to add a hash (called pins) of the certificate into the network_security_config.xml file. Here is an example implementation:
duckduckgo.com lFL47+i9MZkLqDTjnbPTx2GZbGmRfvF3GkEh+J+1F3g= w9MWhhnFZDSPWTFBjaoeGuClsrCs7Z70lG7YNlo8t+s= To find the pins for a specific site, you can go to SSL Labs, enter the site, and click Submit. Or, if you're developing an app for a company, you can ask the company for it.
Note: If you need to support devices running an OS version older than Android N, you can use the TrustKit library. It utilizes the Network Security Configuration file in exactly the same way.
Sanitization and Validation
With all of the protections so far, your connections should be quite secure. Even so, don't forget about regular programming validation. Blindly trusting data received from the network is not safe. A good programming practice is “design by contract”, where the inputs and outputs of your methods satisfy a contract that defines specific interface expectations.
For example, if your server is expecting a string of 48 characters or less, make sure that the interface will only return up to and including 48 characters.
if (editText.getText().toString().length()If you're only expecting numbers from the server, your inputs should check for this. While this helps to prevent innocent errors, it also reduces the likelihood of injection and memory corruption attacks. This is especially true when that data gets passed to NDK or JNI—native C and C++ code.
The same is true for sending data to the server. Don't blindly send out data, especially if it's user-generated. For example, it's good practice to limit the length of user input, especially if it will be executed by an SQL server or any technology that will run code.
While securing a server against attacks is beyond the scope of this article, as a mobile developer, you can do your part by removing characters for the language that the server is using. That way, the input is not susceptible to injection attacks. Some examples are stripping quotes, semicolons and slashes when they're not essential for the user input:
string = string.replace("", "").replace(";", "").replace(""", "").replace("'", "");If you know exactly the format that is expected, you should check for this. A good example is email validation:
private final String emailRegexString = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,4}$"; private boolean isValidEmailString(String emailString) { return emailString != null && Pattern.compile(emailRegexString).matcher(emailString).matches(); }Files can be checked as well. If you're sending a photo to your server, you can check it's a valid photo. The
first two bytes and last two bytes are alwaysFF D8
andFF D9
for the JPEG format.private static boolean isValidJPEGAtPath(String pathString) throws IOException { RandomAccessFile randomAccessFile = null; try { randomAccessFile = new RandomAccessFile(pathString, "r"); long length = randomAccessFile.length(); if (lengthBe careful when displaying an error alert that directly shows a message
from the server. Error messages could disclose private debugging or
security-related information. The solution is to have the server send an
error code that the client looks up to show a predefined message.Communication With Other Apps
While you're protecting communication to and from the device, it's important to protect IPC as well. There have been cases where developers have left shared files or have implemented sockets to exchange sensitive information. This is not secure. It is better to use Intents. You can send data using an Intent by providing the package name like this:
Intent intent = new Intent(); intent.setComponent(new ComponentName("com.example.app","com.example.app.TheActivity")); intent.putExtra("UserInfo", "Example string"); startActivity(intent);To broadcast data to more than one app, you should enforce that only apps signed with your signing key will get the data. Otherwise, the information you send can be read by any app that registers to receive the broadcast. Likewise, a malicious app can send a broadcast to your app if you have registered to receive the broadcast. You can use a permission when sending and receiving broadcasts where signature is used as the protectionLevel. You can define a custom permission in the manifest file like this:
Then you can grant the permission like this:
Both apps need to have the permissions in the manifest file for it to work. To send the broadcast:
Intent intent = new Intent(); intent.putExtra("UserInfo", "Example string"); intent.setAction("com.example.SOME_NOTIFICATION"); sendBroadcast(intent, "com.example.mypermission");Alternatively, you can use
setPackage(String)
when sending a broadcast to restrict it to a set of apps matching the specified package. Settingandroid:exported
tofalse
in the manifest file will exclude broadcasts that are received from outside of your app.End-to-End Encryption
It's important to understand the limits of HTTPS for protecting network communications. In most HTTPS implementations, the encryption is terminated at the server. For example, your connection to a corporation's server may be over HTTPS, but once that traffic hits the server, it is unencrypted. It may then be forwarded to other servers, either by establishing another HTTPS session or by sending it unencrypted. The corporation is able to see the information that has been sent, and in most cases that's a requirement for business operations. However, it also means that the company could pass the information out to third parties unencrypted.
There is a recent trend called "end-to-end encryption" where only the two end communicating devices can read the traffic. A good example is an encrypted chat app where two mobile devices are communicating with each other through a server; only the sender and receiver can read each other's messages.
An analogy to help you understand end-to-end encryption is to imagine that you want someone to send you a message that only you can read. To do this, you provide them with a box with an open padlock on it (the public key) while you keep the padlock key (private key). The user writes a message, puts it in the box, locks the padlock, and sends it back to you. Only you can read the message because you're the only one with the key to unlock the padlock.
With end-to-end encryption, both users send each other their keys. The server only provides a service for communication, but it can't read the content of the communication. While the implementation details are beyond the scope of this article, it's a powerful technology. If you want to learn more about this approach, a great place to start is the GitHub repo for the open-sourced Signal project.
Conclusion
With all the new privacy laws such as GDPR, security is ever more important. It's often a neglected aspect of mobile app development.
In this tutorial, you've covered the security best practices, including using HTTPS, certificate pinning, data sanitization and end-to-end encryption. These best practices should serve as a foundation for security when developing your mobile app. If you have any questions, feel free to leave them below, and while you're here, check out some of my other tutorials about Android app security!
Security
Keys, Credentials and Storage on Android
Collin Stuart
Security
Storing Data Securely on Android
Collin Stuart