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. By Kolin Stürt.

4.7 (7) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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!