Norway


Security is an important part of development. Users expect you to protect their data from unintended prying eyes. has mechanisms in place by default for controlling who can view the information that your app collects on devices, but almost every app communicates over a . You can keep your users’ information private by ensuring that your app is securing 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 that you’re already familiar with the basics of Android networking. If the concepts of networking are new to you, first read through our Android Networking Tutorial.

Getting Started

Download and unzip the materials for this tutorial using the Download materials button at the top or bottom of this page. Open the starter in Android Studio 3.1.3 or higher, and navigate to the PetRequester.kt file. Right now, the retrievePets() method is making a simple call to retrieve JSON data for a list of pets and their medical data.

Build and run the project to see what you’ll be working with.

Starter project  - AppData 1 281x500 - Securing Network Data Tutorial for Android

Understanding HTTPS

Browse through the selection of pets by swiping up on the screen. Tapping the photo of a pet reveals a detailed view of its medical data. Everything looks fine on the surface but, on the first line of the retrievePets() method, you’ll notice that the URL starts with http://.

HTTP data is transmitted in the clear. This means all the medical information about Pom the Pomeranian, for example, was retrieved 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, you’ll change this request to HTTPS. HTTPS uses TLS — or Layer Security — to encrypt data in transit. All you need to do to change this request is to append “s” to the “http” section of the URL string in retrievePets(), and change the connection class to HttpsURLConnection. As long as the host supports HTTPS, a secure connection will be made. That makes it very difficult to use those previously mentioned tools to monitor the data.

This, of course, not only applies to the example of medical data. Login requests, banking details or anything with personally identifiable information (PII) should be sent over HTTPS. But instead of trying to guess what type of information is personal, it’s a better practice to make all requests HTTPS from the beginning. As of Android N, you can enforce this with the Network Security Configuration.

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

Enforcing HTTPS

To enforce HTTPS traffic on Android N and higher, right-click on the app/res directory and select New ▸ Directory. Name it xml. 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 the cleartextTrafficPermitted attribute to false, which will block any network requests that do not use HTTPS, for specific domains that you specify. You then added github.io as a domain, setting its includeSubdomains attribute to true, which will require HTTPS for subdomains like collinstuart.github.io. Now you need to tell the Android system to use this file.

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

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

Build and debug the project again on an Android N (or newer) emulator or device. You should see an error message in the Debug tab that says java.io.IOException: Cleartext HTTP traffic to collinstuart.github.io not permitted:

Error message  - ClearTrafficError 1 650x291 - Securing Network Data Tutorial for Android

That17;s because Android blocked the calls so that no data was retrieved in the clear. Your app should look like this:

App with no data  - AppNoData 2 281x500 - Securing Network Data Tutorial for Android

Sad face  - basic sad 1 - Securing Network Data Tutorial for Android

Now that you have enabled HTTPS enforcement, it’s to fix the violation. At the beginning of the retrievePets() method in the PetRequester.kt file, replace all of the code up until the doAsync block with this:

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

This code breaks out the connection into separate variables that you will work with later. The main differences here are:

  • The URL changed from http:// to https://
  • The connection variable is now of the HttpsURLConnection type.

Build and run the app again. The app displays the data again, this time over HTTPS. That was easy!

Note: Often when security vulnerabilities are found in software, a patch is released. It’s a good idea to make sure the security provider for HTTPS is patched. If, during your debugging, you see an error such as SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure, this usually means the provider needs to be updated as well. For more information about this procedure, see the Update Your Security Provider page.

Understanding Certificate and Public Pinning

Now that you’ve taken the first step in securing the data, take a step back to talk about 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 signed the certificate. A certificate might be signed by an intermediate certificate authority. That intermediate certificate might in turn be signed by another intermediate authority. The connection is secure as long as the first certificate is signed by a root certificate authority that is trusted by Android.

The Android system evaluates that certificate chain. If a certificate is not valid, then 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 a legitimately signed one. For example, a hacker can manually instruct Android to accept their own installed certificate. Or a company may have a work device configured to accept their own certificate. This allows the entity in possession of the certificate to be able to decrypt, read and modify the traffic, called a man-in-the-middle attack.

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

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

Certificate pinning  - SSLPinning 1 - Securing Network Data Tutorial for Android

To get the pin for the host you are talking to, head over to SSL Labs. Type in github.io for the Hostname field and click submit:

SSL Labs site  - SSLLabsStep1 650x279 - Securing Network Data Tutorial for Android

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

github.io servers  - SSLLabsStep2 650x379 - Securing Network Data Tutorial for Android

You’ll see there are two certificates listed, the second one being a backup. Each entry has a Pin SHA256 section:

SHA256  - SSLLabsStep3 509x500 - Securing Network Data Tutorial for Android

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

<pin-set>
  <!--Note: These values may change over time, so be sure to use the values that you obtained from the ssllabs lookup that you did as part of this tutorial -->
  <pin digest="SHA-256">sm6xYAA3V3PtiyWIX6G/FY2kgHCRzR1k9XndcF5A0mg=</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 directly bug IT for it. :]

Implementing TrustKit

You’ve added certificate pinning support for Android N and higher, but what if your app needs to support versions under N? TrustKit is a library that uses the same format in the network_security_config.xml file to add support for versions under Android N.

You’ll now add the TrustKit library to the project. Head over to your app module build.gradle file 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.0.3'

Make sure to sync your Gradle files before proceeding.

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

<trustkit-config enforcePinning="true" />

This tells TrustKit to enable certificate pinning using the existing pins we added above. You need to initialize TrustKit with that security configuration, somewhere near your app startup, before you make any network requests. In the MainActivity.kt file, add the initialization code to the onCreate() method, just before the last line that sets the petRequester variable:

TrustKit.initializeWithNetworkSecurityConfiguration(this)

TrustKit will need to be imported. You can either use option+return on Mac or Alt+Enter on PC, or manually add it to the list of imports at the top of the file:

import com.datatheorem.android.trustkit.TrustKit

Now, go back and tell HttpsURLConnection to involve TrustKit when making a connection. In the PetRequester.kt file, add TrustKit to the list of imports, then add the following right before the doAsync block:

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

The HttpsURLConnection will now use the TrustKit socket factory, which will take care of making sure the certificates match.

Build and run the app. If all went well, the app will display the pets on the screen. To test that everything is working, navigate to the network_security_config.xml file. Change any character besides = for each of the pin digest entries. Here’s an example:

<pin digest="SHA-256">sm6xYAA3V3PtiyWIX6G/FY2kgHCRzR1k9XndcF5A0mz=</pin>
<pin digest="SHA-256">k2v657xBsOVe1PQRwOsHsw3bsGT2VzIqz5K+59sNQwz=</pin>

Build and debug the app. You should now see an error that says javax.net.ssl.SSLHandshakeException: Pin verification failed.

Pin verification failed error  - PinVerifyFailed 1 - Securing Network Data Tutorial for Android

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

There are many other third-party solutions for certificate pinning. For more information about certificate pinning in general, see the OWASP documentation.

Understanding Authentication

During the second world war, 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 mask the German beams. What the Germans needed was some kind of signature to be able to tell the false beams from the authentic ones. We have this today with digital signatures, which verify the integrity of information.

Digital signatures make sure that it’s you accessing your banking data, starting a chat or logging into a service. They also makes sure someone else has not 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 is no computationally feasible way to reverse it to reveal what the original input was.

The output of a hash function will always be the same if the input is the same. The output will be drastically different even if you change one byte or character. That makes it perfect for you to verify that a large amount of data is not corrupted by hashing the data and then comparing that hash with the expected one.

You’ll use Secure Hash Algorithm (SHA), which a well-known standard that refers to a group of hash functions.

Public-Key Cryptography

In many cases, when an API sends data over a network, the data will also contain a signature. But how can you use this to know if a malicious user has tampered with the data? Right now, all an attacker needs to do is alter that data and then recompute the signature. What you need is some secret information added to the mix when hashing the data so that the attacker cannot recompute the signature without knowing that secret. But even if you have a secret, how do both parties let each other know what the secret is without it being intercepted? 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 is used to create the signature, while the public key verifies the signature.

Given a public key, it is 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 needed to reconstruct the signature. The latest and greatest way to do this is through Elliptic- Cryptography (ECC).

Elliptic-Curve Cryptography  - ECDSA 650x457 - Securing Network Data Tutorial for Android

Elliptic-Curve Cryptography

ECC is a new set of algorithms based on elliptic curves over finite fields. While it can be used for encryption, you’ll use it for authentication, which is often referred to as ECDSA (Elliptic Curve Digital Signature Algorithm).

Key  - object tool key - Securing Network Data Tutorial for Android

Right-click on the com.raywenderlich.android.petmed folder 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 a Public and Private Keypair

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

class Authenticator {

  private val publicKey: PublicKey
  private val privateKey: PrivateKey

}

Initializing the Private and Public Keys

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
}

What did you do here?

  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. Put this code right after the init block:

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

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

Now, add the verify method to your class:

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

This time, the Signature object is initialized with the public key that is needed for verification. The signature object is updated with the data to be verified and then the update method is called to do the verification. The method returns true if the verification was successful.

You’ll also need a way to verify data for a public key that is sent to you. Create a second verify method that accepts an external public key:

fun verify(signature: ByteArray, data: ByteArray, publicKeyString: String):
    Boolean {
  val verifySignature = Signature.getInstance("SHA1withECDSA")
  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 method, except that it converts a Base64 public key string into a PublicKey object. Base64 is a format that allows raw data bytes to be easily passed over the network as a string.

Now that you have an Authenticator class, you’ll make use of it inside PetRequester.

Verifying a Signature

In one scenario, apps could be required to register with a service where the public key is passed back; this is often 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, the public key for the Github server that you’re communicating with will be included in the code. It will be used to verify the pet data that comes 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 = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEP9M/My4tmNiaZRcQtYj58EjGN8N3uSnW/s7FpTh4Q+T3tNVkwVCjmDN+a2qIRTcedQyde0d8CoG3Lp2ZlnPhcw=="

Next, create an authenticator instance in the retrievePets() method, right under the first three lines that define the URL:

val authenticator = Authenticator()

Then, replace the contents inside the uiThread block with the following:

// Verify received signature
// 1
val jsonElement = JsonParser().parse(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 = android.util.Base64.decode(signature, android.util.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 are taking all the JSON content for items and turning it into a ByteArray.
  2. You’re also retrieving the signature string that is returned and you are turning that into a ByteArray.
  3. Now, you are using authenticator to verify the data bytes with the signature bytes, given the server’s public key.
  4. If the data is verified, it is passed to the response listener.

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

Success breakpoint  - Authentication1 1 650x343 - Securing Network Data Tutorial for Android

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

resultBytes[resultBytes.size - 1] = 0

That code will replace the last byte of the received data with 0. Build and run the app again. This time, no data will be displayed because success will be false:

Success breakpoint  - Authentication2 1 650x334 - Securing Network Data Tutorial for Android

Don’t forget to remove that test after you’re done.

Signing a Request

Another common scenario is when you’re connecting to a server with a back-end API. Often, you’ll need to register by sending your public key before being able to access a specific endpoint, such as /send_message. A PublicKey‘s bytes can be retrieved by calling publicKey.encoded. The app then needs to sign its request to the /send_message endpoint in order to successfully use it.

When signing a request, it’s common practice to take selected parts of the request, such as HTTP Headers, GET or POST parameters and the URL, and join them together into a string. That string is used to create the signature. On the backend side, 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 other users are able to impersonate the user because they do not have that private key.

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

You’ll create a simple signature for the pets’ request. Back in PetRequester.kt, add the following code to the retrievePets() method, just under the line that sets the authenticator value:

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

Here:

  1. You take the request string and turn it into a ByteArray.
  2. The bytes get signed using the internal private key and the signature bytes are returned.
  3. You turn the signature bytes into a Base64 string so that it can easily be sent 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 the app to see the result in the Debug tab.

Signature for request verified  - Authentication3 1 650x282 - Securing Network Data Tutorial for Android

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

bytesToSign[bytesToSign.size - 1] = 0

Build and run the app. This time, success will be false in the Debug tab.

Signature for request failed  - Authentication4 1 650x283 - Securing Network Data Tutorial for Android

Congratulations! You just secured the data with a signature.

Happy face  - basic happy 3 - Securing Network Data Tutorial for Android

While you have been verifying the integrity of the data, it’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 4096 bits), and key generation is slower. You might use it if the rest of your team is already familiar with or using the standard.
  • HMAC is another popular solution that, instead of using public-key cryptography, relies on a single, shared key. The secret key must be exchanged securely. HMAC is used 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 have secured your connection to a server, the traffic is decrypted once it arrives. Usually, 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. The best way to explain end-to-end encryption is a chat app where only the sender and receiver have the keys to decrypt each others’ messages. The service or company has no way of knowing what the content is. This is a great way to avoid being liable in the event of 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.

To learn more about Android security in general, check out the Security For Android Developers page.

And, of course, to check out more about Pom The Pomeranian, you can find him on Instagram. :]

If you have any questions about what has been covered, please join the discussion below!



Source link

LEAVE A REPLY

Please enter your comment!
Please enter your name here