Home · Android & Kotlin Tutorials

Securing Network Data Tutorial for Android

In this Android tutorial, you’ll learn how to keep your information private by securing network data in transit.

Version

  • Kotlin 1.3, Android 4.4, Android Studio 3.6
Update note: Kolin Stürt updated this tutorial. He also wrote the original.

Security is an important part of development. With more and more people turning to apps to work, users expect you to protect their data from prying eyes. Almost every app communicates over a network. You can keep your user’s information private by ensuring that your app is securing network data in transit.

In this tutorial, you’ll secure a simple Android app named PetMed for veterinary clinics that exchange medical information over a network.

During the process, you’ll learn the following best practices:

  • Using HTTPS for network calls.
  • Trusting a connection with certificate pinning.
  • Verifying the integrity of transmitted data.
Note: This tutorial assumes you’re already familiar with the basics of Android networking. If this subject is new to you, read our Android Networking Tutorial first.

Getting Started

Download the project materials for this tutorial using the Download Materials button at the top or bottom of this page, then unzip them. Open the starter project in Android Studio 3.6.2 or higher and navigate to PetRequester.kt.

Right now, retrievePets() makes a simple call to retrieve JSON data for a list of pets and their medical data.

Build and run the project on an emulator or device with a network connection to see what you’re working with. Browse through the selection of pets. You’ll find that tapping an entry reveals a detailed view of medical data.

PetMed Details View

Everything looks fine on the surface, but your job is to ensure this medical data is secure.

Understanding HTTPS

URLs that start with http:// transmit data unprotected for anyone to view. Many popular tools are available to monitor HTTP traffic.

Some examples are Wireshark, mitmproxy and Charles.

Because Pomeranians tend to be fussy about their privacy, the requests in this app use HTTPS. See for yourself by looking at the first line of retrievePets.

HTTPS uses Transport Layer Security (TLS) to encrypt network data in transit, an important layer of protection.

All you need to do to ensure a request uses TLS is to append “s” to the “http” section of a URL. That makes it very difficult to use those previously-mentioned tools to monitor the data.

However, this doesn’t provide perfect protection. You’ll want to take some additional steps to ensure your users’ privacy.

Using Perfect Forward Secrecy

While encrypted traffic is unreadable, IT companies can still store it. If attackers compromise the key encrypting your traffic, they can use it to read all the previously-stored traffic.

To prevent this vulnerability, Perfect Forward Secrecy (PFS) generates a unique session key for each communication session. If an attacker compromises the key for a specific session, it won’t affect data from other sessions.

Android 5.0+ implements PFS by default. It prohibits TLS ciphers that don’t support PFS.

Note: It’s also a good practice to limit the amount of data you send from your app to just the essentials.

As of Android N, you can enforce this by using Network Security Configuration. You’ll add this security to your app in the next section.

Enforcing TLS With Network Security Configuration

To enforce TLS on Android N and higher, right-click on app/res and select New ▸ Directory. Name it xml.

Then right-click on the newly-created directory and select New ▸ File. Name it network_security_config.xml.

In the newly-created file, add the following code:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <domain-config cleartextTrafficPermitted="false">
    <domain includeSubdomains="true">github.io</domain>
  </domain-config>
</network-security-config>

Here, you set cleartextTrafficPermitted to false, which blocks any network requests that do not use TLS, for specific domains that you specify.

You then added github.io as a domain and set its includeSubdomains attribute to true. This will enforce TLS be required for subdomains like kolinsturt.github.io.

Next, you need to tell the Android system to use this file.

In AndroidManifest.xml, replace the beginning <application tag with this line:

<application android:networkSecurityConfig="@xml/network_security_config"

To test that it worked, replace the first statement of retrievePets in PetRequester.kt with this:

val connection =
    URL("http://kolinsturt.github.io/posts.json").openConnection() as HttpURLConnection

Here, you changed the URL to use HTTP and HttpsURLConnection to HttpURLConnection in order to test what happens when you send data without encryption.

Build and debug the project again in an emulator or device running Android N or newer. You'll see an error message in Debug that says java.io.IOException: Cleartext HTTP traffic to kolinstuart.github.io not permitted:

PetMed Cleartext Error

That's because Android blocked the calls so it won't retrieve unencrypted data. Your app will either crash or look like this:

App with no data

Sad face

Undo that change so that the code is back to this:

val connection =
    URL("https://kolinsturt.github.io/posts.json").openConnection() as HttpsURLConnection

Build and run the app. The app displays the data again, but this time, you know it enforced TLS.

That was easy, but there are a few more simple changes you can do to make your app even more secure.

Note: Often, when security researchers find vulnerabilities in software, the software company releases a patch. It's a good idea to make sure you've patched the security provider for TLS. If you see an error such as SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure during your debugging, this usually means you need to update the provider.

For more information about this procedure, see the Update Your Security Provider page.

Understanding Certificate and Public Key Pinning

Now that you've taken the first step in securing the data, take a moment to consider how HTTPS works.

When you start an HTTPS connection, the server presents a certificate that verifies it's the real entity. This is possible because a trusted certificate authority (CA) signed the certificate.

An intermediate authority might also have signed an intermediate certificate — there can be more than one signature. The connection is secure as long as a root certificate authority that Android trusts signed the first certificate.

The Android system evaluates that certificate chain. If a certificate isn't valid, it closes the connection.

That sounds good, but it's far from foolproof. Many weaknesses exist that can make Android trust an attacker's certificate instead of one that's legitimately signed.

For example, a company might have a work device configured to accept its own certificate. Or a hacker can manually instruct Android to accept their installed certificate. This is called a man-in-the-middle attack — it allows the entity in possession of the certificate to decrypt, read and modify the traffic.

Certificate pinning comes to the rescue by preventing connections when these scenarios occur. It works by checking the server's certificate against a copy of the expected certificate.

Implementing Certificate Pinning

Fortunately, this is easy to implement on Android N+. Instead of comparing the entire certificate, it compares the hash (more on this later) of the public key, often called a pin.

Certificate pinning

To get the pin for the host you're talking to, head to SSL Labs. Type github.io for the Hostname field and click Submit:

SSL Server Test page on the SSL Labs site

On the next page, select one of the servers from the list:

List of github.io servers

You'll see there are two certificates listed; the second one is a backup. Each entry has a Pin SHA256 value:

Certificates with their Pin SHA256 section highlighted

Note: These values may change over time, so be sure to lookup these values before using them

Those are the hashes of the public keys that you'll add into the app. Go back into network_security_config.xml and add them right after the domain tag for github.io:

<pin-set>
  <pin digest="SHA-256">xlDAST56PmiT3SR0WdFOR3dghwJrQ8yXx6JLSqTIRpk=</pin>
  <pin digest="SHA-256">k2v657xBsOVe1PQRwOsHsw3bsGT2VzIqz5K+59sNQws=</pin>
</pin-set>
Note: There are many ways to get the public key hash. One alternative is to download the certificate directly from the website and run OpenSSL commands on it. If you're developing an app for a company, you might bug IT for it directly. :]

With that you've added certificate pinning support for Android N and higher, but what if your app needs to support versions under N? You will handle this case next by using TrustKit.

Implementing TrustKit

TrustKit is a library that uses the same format in network_security_config.xml to add support for versions under Android N.

You'll now add the TrustKit library to the project. Head to your app module build.gradle and add this to your list of dependencies:

implementation "com.datatheorem.android.trustkit:trustkit:$trustkit_version"

Next, add the TrustKit version to your project level build.gradle file at the beginning of the buildscript block:

ext.trustkit_version = '1.1.2'

Be sure to sync your Gradle files before proceeding.

Then, in network_security_config.xml, add the following right after the pin-set section:

<trustkit-config enforcePinning="true" />

This tells TrustKit to enable certificate pinning using the existing pins you added above. You need to initialize TrustKit with that security configuration somewhere near your app startup, before you make any network requests.

In MainActivity.kt, add the initialization code to onCreate(), just before the last line that sets petRequester (import TrustKit when required):

TrustKit.initializeWithNetworkSecurityConfiguration(this)

Now, go back and tell HttpsURLConnection to involve TrustKit when making a connection. In PetRequester.kt, add the following right after connection declaration line:

connection.sslSocketFactory = TrustKit.getInstance().getSSLSocketFactory(connection.url.host)

HttpsURLConnection will now use the TrustKit socket factory, which will make sure the certificates match.

If you build and run the app, you will see no change.

To test that everything is working, navigate to network_security_config.xml. Change any character other than = for each of the pin digest entries. Here's an example:

<pin digest="SHA-256">klDAST56PmiT3SR0WdFOR3dghwJrQ8yXx6JLSqTIRpk=</pin>
<pin digest="SHA-256">m2v657xBsOVe1PQRwOsHsw3bsGT2VzIqz5K+59sNQws=</pin>

If you build and run, you'll see an error that says either com.datatheorem.android.trustkit.reporting.BackgroundReporter.pinValidationFailed or javax.net.ssl.SSLHandshakeException: Pin verification failed.

Pin verification failed error

You just successfully added certificate pinning to your app! Don't forget to undo those changes that caused pin verification to fail. :]

For more information about certificate pinning in general, see the OWASP documentation.

While pinning is popular, some companies don't like that they have to update their apps from time to time with new pins. That's a problem that Certificate Transparency solves.

Using Certificate Transparency

Certificate Transparency is a new standard that audits the certificates presented during the setup of an HTTPS connection without requiring hard-coded values in the app.

When a CA issues a certificate, it must submit it to a number of append-only certificate logs. Certificate Transparency has nearly real-time monitoring to determine if someone has compromised the CA or if the CA issued the certificate maliciously.

The owner of the domain can scrutinize the entries and your app cross-checks the logs. The certificate is only valid if it exists in at least two logs.

When an entity revokes a certificate in a security situation, you want to know about it immediately. You can use Certificate Transparency on top of pinning for greater security. You'll add it to your app next.

In the app module build.gradle file, add the following to the list of dependencies and sync Gradle:

implementation 'com.babylon.certificatetransparency:certificatetransparency-android:0.2.0'

Next, navigate to PetRequester.kt file. In retrievePets, find the line that declares connection. Add the following right under that line (import certificateTransparencyHostnameVerifier when required):

connection.hostnameVerifier = // 1
    certificateTransparencyHostnameVerifier(connection.hostnameVerifier) {
      // Enable for the provided hosts
      +"*.github.io" // 2

      // Exclude specific hosts
      //-"kolinsturt.github.io" // 3
    }

Here, you:

  1. Enabled Certificate Transparency.
  2. Added a wildcard (*) site for GitHub using +. This means you enabled Certificate Transparency on all domains that end in github.io.
  3. Excluded specific domains using -. This example allows all GitHub domains except the one starting with kolinsturt.

You should be able to build and run the app without any issue.

But we are not done yet. Next, you'll learn about a few more options that affect certificate checking.

Stopping Information Leaks With OCSP Stapling

The traditional way to determine if an entity revoked a certificate is to check a Certificate Revocation List (CRL). To do this, your app must contact a third party to confirm the validity of the certificate, which adds network overhead. It also leaks private information about the sites you want to connect with to the third party.

Here Online Certificate Status Protocol (OCSP) Stapling comes to the rescue. When you start an HTTPS request to the server using this method, the validity of the server's certificate is already "stapled" to the response.

OCSP Stapling is enabled by default, but you can disable it or customize the behavior of certificate revocation using PKIXRevocationChecker.Option. See commented code inside the PetRequester.kt's init block in the final project for sample code.

The server you're connecting to can't forge this info. The CA signs that info ahead of time, so it doesn't know which site you want to access.

What is signing, you ask? It's a way to verify the integrity of data. Even though you've encrypted data, how do you know it's authentic in the first place? You'll now use authentication to ensure the integrity of the information you send and receive over the network.

Understanding Authentication

During World War II, German bombers used Lorenz radio beams to navigate and to find targets in Britain. A problem with this technology was that the British started transmitting their own, stronger, beams on the same wavelength to confuse the German beams. What the Germans needed was some kind of signature to be able to tell the false beams from the authentic ones.

Today, developers use digital signatures for a similar purpose — to verify the integrity of information.

Digital signatures make sure that you're the one accessing your health data, starting a chat or logging into a bank. They also ensure no one has altered the data.

At the heart of a digital signature is a hash function. A hash function takes a variable amount of data and outputs a signature of a fixed length. It's a one-way function. Given the resulting output, there's no computationally-feasible way to reverse it to reveal what the original input was.

The output of a hash function is always the same if the input is the same. The output is drastically different if you change even one byte or character. That makes it the perfect way to verify that a large amount of data isn't corrupted. You simply hash the data and compare that hash with the expected one.

Next, to authenticate if data was not tampered with, you'll use a Secure Hash Algorithm (SHA), which is a well-known standard that refers to a group of hash functions.

Authenticating With Public-Key Cryptography

In many cases, when an API sends data over a network, the data also contains a signature. But how can you use this to know if a malicious user has tampered with the data? All an attacker needs to do is alter that data and then recompute the signature.

What you need is to add some secret information to the mix when you hash the data. The attacker cannot recompute the signature without knowing the secret. But how do both parties let each other know what the secret is without someone intercepting it? That's where Public-Key cryptography comes into the picture.

Public-Key cryptography works by creating a set of keys, one public and one private. The private key creates the signature, while the public key verifies the signature.

Given a public key, it's not computationally feasible to derive the private key. Even if malicious users know the public key, all they can do is to verify the integrity of the original message. Attackers can't alter a message because they don't have the private key to reconstruct the signature. The latest and greatest way to do this is through Elliptic-Curve Cryptography (ECC).

How signing and verifying work in Elliptic-Curve Cryptography

Verifying Integrity With Elliptic-Curve Cryptography

ECC is a new set of algorithms based on elliptic curves over finite fields. While you can use it for encryption, in this tutorial, you'll use it for authentication, known as ECDSA (Elliptic Curve Digital Signature Algorithm).

To start using ECDSA, right-click on com.raywenderlich.android.petmed and select New ▸ Kotlin File/Class. Call it Authenticator and select Class for the Kind. At the top of the file, below the package declaration, import the necessary key and factory classes:

import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import java.security.Signature
import java.security.spec.X509EncodedKeySpec

Adding the Public and Private Keys

Add a public and private key pair to the class so that it looks like the following:

class Authenticator {

  private val publicKey: PublicKey
  private val privateKey: PrivateKey

}

You need to initialize these private and public keys. Right after the variables, add the init block:

init {
  val keyPairGenerator = KeyPairGenerator.getInstance("EC") // 1
  keyPairGenerator.initialize(256) // 2
  val keyPair = keyPairGenerator.genKeyPair() // 3
    
  // 4
  publicKey = keyPair.public
  privateKey = keyPair.private
}

Here's what you did with this code:

  1. Created a KeyPairGenerator instance for the Elliptic Curve (EC) type.
  2. Initialized the object with the recommended key size of 256 bits.
  3. Generated a key pair, which contains both the public and private key.
  4. Set the publicKey and privateKey variables of your class to those newly-generated keys.

Adding the Sign and Verify Methods

To complete this class, add the sign and verify methods. Define sign method right after the init block:

fun sign(data: ByteArray): ByteArray {
  val signature = Signature.getInstance("SHA512withECDSA")
  signature.initSign(privateKey)
  signature.update(data)
  return signature.sign()
}

This method takes in a ByteArray. It initializes a Signature object with the private key for signing, adds the ByteArray data and then returns a ByteArray signature.

Now, add verify method below sign method:

fun verify(signature: ByteArray, data: ByteArray): Boolean {
  val verifySignature = Signature.getInstance("SHA512withECDSA")
  verifySignature.initVerify(publicKey)
  verifySignature.update(data)
  return verifySignature.verify(signature)
}

This time, the method initializes a Signature object with the public key for verification. It updates the signature object with your data and verify performs the verification. The method returns true if the verification succeeded.

You'll also need a way to verify data given a public key you receive. Create a second verify that accepts an external public key:

fun verify(
    signature: ByteArray, 
    data: ByteArray, 
    publicKeyString: String
): Boolean {
  val verifySignature = Signature.getInstance("SHA512withECDSA")
  val bytes = android.util.Base64.decode(publicKeyString,
      android.util.Base64.DEFAULT)
  val publicKey =
      KeyFactory.getInstance("EC").generatePublic(X509EncodedKeySpec(bytes))
  verifySignature.initVerify(publicKey)
  verifySignature.update(data)
  return verifySignature.verify(signature)
}

This code is similar to the previous verify, except that it converts a base 64 public key string into a PublicKey object. Base64 is a format that allows you to pass raw data bytes over the network as a string.

Now that you have an Authenticator, you'll use it inside PetRequester.

Verifying a Signature

In one scenario, apps could register with a service where it passes the public key back. This is called a token or secret. For a chat app, for example, each user might exchange public keys upon initiating a chat session.

In this example, you'll include the public key for the GitHub server that you're communicating with. You'll use it to verify the pet data originating from the items JSON list.

Open PetRequester.kt and add the public key to the top of the file, just under the import statements:

private const val SERVER_PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZmhp0EzuDRq0FK0AcV/10RzrTYp+HiGU457hCNgcn0uun0gYz1rmhsAZaieQoiqubCgXwP/XkVKYKOZ8CHGkWA=="

Next, create an authenticator instance in retrievePets(), right after the connection.hostnameVerifier block you added earlier:

val authenticator = Authenticator()

Then, replace the contents inside withContext(Main) with the following:

// Verify received signature
// 1
val jsonElement = JsonParser.parseString(json)
val jsonObject = jsonElement.asJsonObject
val result = jsonObject.get("items").toString()
val resultBytes = result.toByteArray(Charsets.UTF_8)

// 2
val signature = jsonObject.get("signature").toString()
val signatureBytes = Base64.decode(signature, Base64.DEFAULT)

// 3
val success = authenticator.verify(signatureBytes, resultBytes, SERVER_PUBLIC_KEY)

// 4
if (success) {
  // Process data
  val receivedPets = Gson().fromJson(json, PetResults::class.java)
  responseListener.receivedNewPets(receivedPets)
}

Here's what’s going on in the updated block:

  1. You take the JSON content for items and turn it into a ByteArray.
  2. You also retrieving the returned signature string and turn it into a ByteArray.
  3. Now, you use authenticator to verify the data bytes with the signature bytes, given the server's public key.
  4. If the authenticator verifies the data, you pass that data to the response listener.

Debug and run to check that it worked. Set a breakpoint on the if (success) { line to check that success is true:

Success breakpoint

To test what happens when there are problems, alter the data the app receives. Add the following right after val resultBytes = result.toByteArray(Charsets.UTF_8):

resultBytes[resultBytes.size - 1] = 0

Above line of code replaces the last byte of the data with 0.

Debug and run again. This time, the app won't display the data because success is false:

Test fails because success is false

Invalid signature

Don't forget to remove the test code line added last, after you're done!

Signing a Request

Another common scenario occurs when you connect to a server with a back-end API. Often, you'll need to register by sending your public key before you can access a specific endpoint, such as /send_message.

You can retrieve a PublicKey's bytes by calling publicKey.encoded. The app then needs to sign its request to the /send_message endpoint to use it successfully.

When signing a request, it's common to take selected parts of the request, such as HTTP Headers, GET or POST parameters, and the URL and join them into a string. You use that string to create the signature.

On the back end, the server repeats the process of joining the strings and creating a signature. If the signatures match, it proves that the user must have possession of the private key. No one can impersonate the user because they don't have that private key.

Since specific parameters of the request are part of the string, it also guarantees the integrity of the request. It prevents attackers from altering the request parameters. For example, a bank wouldn't be happy if attackers could alter the destination account number for a money transfer, or alter the mailing address to receive the victim's credit statements in the mail.

For your next step in making the app more secure, you'll create a simple signature for the pets' request.

Back in PetRequester.kt, add the following code to retrievePets(), just under the line that declares authenticator:

val bytesToSign = connection.url.toString().toByteArray(Charsets.UTF_8) // 1
val signedData = authenticator.sign(bytesToSign) // 2
val requestSignature = Base64.encodeToString(signedData, Base64.DEFAULT) // 3
Log.d("PetRequester", "signature for request : $requestSignature")

Here's what this code does:

  1. You take the request string and turn it into a ByteArray.
  2. The app signs the bytes using the private key and returns the signature bytes.
  3. You turn the signature bytes into a base 64 string so that you can easily send it over the network.

Now, add the following lines to verify that the signature works:

val signingSuccess = authenticator.verify(signedData, bytesToSign)
Log.d("PetRequester", "success : $signingSuccess")

Build and run to see the result in the Debug tab.

Signature for request verified

Now, you'll alter the request data to see what happens. Add the following code right before calling authenticator.verify():

bytesToSign[bytesToSign.size - 1] = 0

Build and run. This time, success is false in the Debug tab.

Signature for request failed

Congratulations! You just secured the data with a signature.

Happy face

Additional Security Considerations

You've been verifying the integrity of the data, but that's not a replacement for regular data validation checks such as type and bounds checking. For example, if your method expects a string of 128 characters or less, you should still check for this.

You should also be aware of a few other standards:

  • RSA is a popular and accepted standard. Its key sizes must be much larger, such as 4096 bits, and key generation is slower. You might use it if the rest of your team is already familiar with or using this standard.
  • HMAC is another popular solution that, instead of using public-key cryptography, relies on a single, shared key. You must exchange the secret key securely. Developers use HMAC when speed considerations are very important.

Where to Go From Here?

You've just secured an app for dealing with sensitive medical data. Download the final project using the Download Materials button at the top or bottom of this tutorial.

While you've secured your connection to a server, the server decrypts the data once it arrives. Sometimes it's a requirement for a company to be able to see this information, but there is a recent trend towards end-to-end encryption.

A good example of end-to-end encryption is a chat app where only the sender and receiver have the keys to decrypt each others' messages. The chat service has no way of knowing what the content is. This makes it a great way to avoid liability for a server-side data breach or compromise.

To learn more about implementing this approach, a good place to start is the open-source Signal App GitHub repo.

In this tutorial, you've been busy securing the network data in transit. Next, you should also protect the stored data and take the network cache into consideration.

Read our Encryption Tutorial For Android and Data Privacy for Android Tutorial to learn about that.

Google has a network security testing tool to help you spot cleartext traffic or other connection vulnerabilities in your app. Visit nogotofail for more info.

For additional security tools, check out the SafetyNet API. It includes a safe browsing, integrity and reCAPTCHA API to protect your app from spammers, phishing URLs and other malicious traffic.

Finally, to check out Pom The Pomeranian, find him on Instagram. :]

If you have any questions about this tutorial, please join the discussion below!

Add a rating for this content

More like this

Contributors

Comments