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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Securing Network Data Tutorial for Android
30 mins
- Getting Started
- Understanding HTTPS
- Using Perfect Forward Secrecy
- Enforcing TLS With Network Security Configuration
- Understanding Certificate and Public Key Pinning
- Implementing Certificate Pinning
- Implementing TrustKit
- Using Certificate Transparency
- Stopping Information Leaks With OCSP Stapling
- Understanding Authentication
- Authenticating With Public-Key Cryptography
- Verifying Integrity With Elliptic-Curve Cryptography
- Verifying a Signature
- Signing a Request
- Additional Security Considerations
- Where to Go From Here?
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).
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:
- Created a
KeyPairGenerator
instance for the Elliptic Curve (EC) type. - Initialized the object with the recommended key size of 256 bits.
- Generated a key pair, which contains both the public and private key.
- Set the
publicKey
andprivateKey
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:
- You take the JSON content for
items
and turn it into aByteArray
. - You also retrieving the returned signature string and turn it into a
ByteArray
. - Now, you use
authenticator
to verify the data bytes with the signature bytes, given the server's public key. - 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
:
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
:
Don't forget to remove the test code line added last, after you're done!