Information: The Foundation of an App
The basis of all meaningful applications is information, and we design and build applications to exchange, create, or store it. Mobile applications are no different. In today’s well-connected mobile landscape, information exchange is the name of the game. To illustrate this point, imagine an Android phone without mobile network or WiFi coverage. While there would still be uses for such a phone, you would have lost access to some of the more important applications on your device. For example, e-mail, instant messaging, web browsing, and any other application that require the Internet would now be nonfunctional.
In later chapters, we will focus our efforts on examining information in transit and how to secure it. In this chapter, we will focus mostly on what happens to information that is stored.
Securing Your Application from Attacks
When created or received, data needs to be stored somewhere. How this information is stored will ultimately reflect on how secure your application really is. Releasing your application to the public should be approached with the same caution and paranoia as launching a website on the Internet. You should assume that your application will be either directly or indirectly attacked at some time and that the only thing standing between your end user’s privacy and data protection is your application.
As dramatic as that last sentence sounds, it is not without basis. Before we go further, let’s take a look at whether my fear mongering is justified. In the latter part of 2010 and early 2011, two vulnerabilities were discovered in Android versions 2.2 and 2.3, respectively. The vulnerability is essentially the same one, in which an attacker can copy any file that is stored on the device’s SD Card without permission or even without a visible cue that this is happening. The vulnerability works as shown in Figure 2-1.
Figure 2-1. Data theft vulnerabilities
The following are the most noteworthy points:
For the sake of argument, assume that your application writes all saved information to the SD Card for storage under its own directory. Because of the vulnerability just discussed, the data used by your application is at risk of being stolen. Any Android device that runs your application and the vulnerable firmware versions poses a risk of data theft to its end user. This is an example of an indirect attack on your application.
How vulnerable your application is to an indirect attack depends largely on how much effort you put into architecting and considering security aspects before you begin writing a single line of code. You may ask the question, “I’m just a small app developer planning to sell my app for a low price online, so do I really need to waste time doing so much planning beforehand?” And I would answer you with a resounding, “Yes!” Whether you are part of a team of thirty developers or an individual working from home, a well-architected application is something you should always strive to create. I hope that this is what you will learn from this book.
Direct attacks are significantly different and can take many different forms. A direct attack can be classified as one that is targeted directly at your application. Thus, the attacker is looking to leverage weaknesses in your application design to either collect sensitive information on your application’s users or to attack the server that your application talks to. Take, for instance, a mobile-banking application. An attacker may go after the mobile applications belonging to a specific bank. If the application design is weak—for example, if that sensitive user data is stored in clear text, or the communication between application and server is not secured by SSL—then an attacker can craft special attacks that only target these weaknesses. This is a direct attack on a specific application. I will cover direct attacks in more detail in Chapter 9 of this book.
Project 1:“Proxim” and Data Storage
Let’s get started with a simple example called Proxim. I’ve been contracted to write an application that can send an SMS to specific, defined contacts when a user is within certain proximity to a set of GPS coordinates. For instance, with this application, a user can add his wife as a contact and have the application SMS her every time he is within three miles of his workplace and house. This way, she knows when he is close to home and the office.
You can download and examine the entire source code for the Proxim application from the Source Code/Download area of the Apress website (www.apress.com). For the sake of clarity, let’s take a look at the most important areas.
The data-storage routine is shown in Listing 2-1.
Listing 2-1. The Save Routine, SaveController. java
package net.zenconsult.android.controller;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import net.zenconsult.android.model.Contact;
import net.zenconsult.android.model.Location;
import android.content.Context;
import android.os.Environment;
import android.util.Log;
public class SaveController {
private static final String TAG = "SaveController";
public static void saveContact(Context context, Contact contact) {
if (isReadWrite()) {
try {
File outputFile = new File(context.getExternalFilesDir(null),contact.getFirstName());
FileOutputStream outputStream = new FileOutputStream(outputFile);
outputStream.write(contact.getBytes());
outputStream.close();
} catch (FileNotFoundException e) {
Log.e(TAG,"File not found");
} catch (IOException e) {
Log.e(TAG,"IO Exception");
}
} else {
Log.e(TAG,"Error opening media card in read/write mode!");
}
}
public static void saveLocation(Context context, Location location) {
if (isReadWrite()) {
try {
File outputFile = new File(context.getExternalFilesDir(null),location.getIdentifier());
FileOutputStream outputStream = new FileOutputStream(outputFile);
outputStream.write(location.getBytes());
outputStream.close();
} catch (FileNotFoundException e) {
Log.e(TAG,"File not found");
} catch (IOException e) {
Log.e(TAG,"IO Exception");
}
} else {
Log.e(TAG,"Error opening media card in read/write mode!");
}
}
private static boolean isReadOnly() {
Log.e(TAG,Environment
.getExternalStorageState());
return Environment.MEDIA_MOUNTED_READ_ONLY.equals(Environment
.getExternalStorageState());
}
private static boolean isReadWrite() {
Log.e(TAG,Environment
.getExternalStorageState());
return Environment.MEDIA_MOUNTED.equals(Environment
.getExternalStorageState());
}
}
Each time a user selects the Save Location button or the Save Contact button, it triggers the preceding code. Let’s take a look at the Location (see Listing 2-2) and Contact (see Listing 2-3) classes in more detail. While we could implement one main save routine, I am keeping it separate in case there is a need to act on different objects in a different manner.
Listing 2-2. The Location Class, Location.java
package net.zenconsult.android.model;
publicclass Location {
private String identifier;
privatedouble latitude;
privatedouble longitude;
public Location() {
}
publicdouble getLatitude() {
return latitude;
}
publicvoid setLatitude(double latitude) {
this.latitude = latitude;
}
publicdouble getLongitude() {
return longitude;
}
publicvoid setLongitude(double longitude) {
this.longitude = longitude;
}
publicvoid setIdentifier(String identifier) {
this.identifier = identifier;
}
public String getIdentifier() {
return identifier;
}
public String toString() {
StringBuilder ret = new StringBuilder();
ret.append(getIdentifier());
ret.append(String.valueOf(getLatitude()));
ret.append(String.valueOf(getLongitude()));
return ret.toString();
}
publicbyte[] getBytes() {
return toString().getBytes();
}
}
Listing 2-3. The Contact Class, Contact.java
package net.zenconsult.android.model;
publicclass Contact {
private String firstName;
private String lastName;
private String address1;
private String address2;
private String email;
private String phone;
public Contact() {
}
public String getFirstName() {
return firstName;
}
publicvoid setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
publicvoid setLastName(String lastName) {
this.lastName = lastName;
}
public String getAddress1() {
return address1;
}
publicvoid setAddress1(String address1) {
this.address1 = address1;
}
public String getAddress2() {
return address2;
}
publicvoid setAddress2(String address2) {
this.address2 = address2;
}
public String getEmail() {
return email;
}
publicvoid setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
publicvoid setPhone(String phone) {
this.phone = phone;
}
public String toString() {
StringBuilder ret = new StringBuilder();
ret.append(getFirstName() + "|");
ret.append(getLastName() + "|");
ret.append(getAddress1() + "|");
ret.append(getAddress2() + "|");
ret.append(getEmail() + "|");
ret.append(getPhone() + "|");
return ret.toString();
}
publicbyte[] getBytes() {
return toString().getBytes();
}
}
The Location and Contact classes are standard classes designed to hold data specific to each type. Each of them contains toString() and getBytes() methods that return the entire contents of the class as either a String or an array of bytes.
If we were to manually add a Contact object, then we would most likely use code similar to what is shown in Listing 2-4.
Listing 2-4. Code that Adds a New Contact Object
final Contact contact = new Contact();
contact.setFirstName("Sheran");
contact.setLastName("Gunasekera");
contact.setAddress1("");
contact.setAddress2("");
contact.setEmail("[email protected]");
contact.setPhone("12120031337");
Assume for the moment that the code in Listing 2-4 is called when a user fills in the screen to add a new contact to the application. Rather than seeing hardcoded values, you will use the getText() methods from each of the EditText objects that are displayed on your main View.
If you execute the code SaveController.saveContact(getApplicationContext(), contact))in your Android simulator, the SaveController will take the newly created Contact and store it in the external media source (refer back to Listing 2-1).
Note It is always good practice to use the getExternalFilesDir() method to find the location of the SD Card on an Android device. Because Android can run on a large number of devices with different specifications, the location of the SD Card directory may not always be in /sdcard. The getExternalFilesDir() method will query the operating system for the correct location of the SD Card and return the location to you.
Let’s take it a line at a time, beginning with the constructor for the saveContact() method:
public static void saveContact(Context context, Contact contact) {
if (isReadWrite()) {
try {
The preceding snippet expects a Context object and a Contact object. Each application on Android has its own Context. A Context object holds application-specific classes, methods, and resources that can be shared among all the classes within an application. For example, a Context object will contain information about the location of the SD Card directory. To access it, you have to invoke the Context.getExternalFilesDir() method. After the method accepts the parameters, it will check to see if the SD Card on the device is mounted and if it is writeable. The isReadWrite() method will execute and return a true or false value to indicate this:
File outputFile = new File(context.getExternalFilesDir(null),contact.getFirstName());
This code creates a File object that points to the location of the SD Card directory. We use the first name of the Contact object as the file name:
FileOutputStream outputStream = new FileOutputStream(outputFile);
outputStream.write(contact.getBytes());
outputStream.close();
Using this code, we create a FileOutputStream that points to the location of our File object. Next, we write the contents of our Contact object to the output stream using the getBytes() method to return an array of bytes. Finally, we close the FileOutputStream.
When execution completes, we should have a file with the name “Sheran” written to the SD Card directory on the device. I’m using the Android simulator on Mac OS X Snow Leopard. Therefore, when I navigate to the location of the simulator, I can see the screen shown in Figure 2-2.
Figure 2-2. The SD Card image file on Max OS X
When this image is mounted by navigating to Android/data/net.zenconsult.android/files, the newly created contact file name is visible (see Figure 2-3).
Figure 2-3. The Contact object that was written to a file
If we open the file up in a text editor, we can see the plain text data that was saved from the application (see Figure 2-4).
Figure 2-4. The contents of the Contact object
One of the things I struggled with when starting out in mobile-application development was the fact that I’d get into code writing from the get go. I’d make up the features in my head and code them as I went along. All too often, I would later spend time revising my code and going back to write a plan midstream. This had devastating effects on my deadlines and deliverables. It also had a detrimental effect on the security of my applications.
I have since learned that writing up a brief outline of the project that I am about to embark on will help me think of things ahead of time. While this seems like an obvious thing, there are many developers that I have spoken with who fail to follow this simple step. One other thing that I have also begun doing religiously is finding time to look at the information or data that my application will be handling. For instance, I use a table like the one shown in Table 2-1 to classify the data that my application handles. The table is very basic; however, by putting it down on paper, I am able to visualize the types of data my application will handle—moreover, I’m able to formulate a plan to secure that information.
Table 2-1. Data Classification Table
If you look at the data classification table in Table 2-1 closely, you will realize that some of the headings are very subjective. Different people will have different opinions on what constitutes sensitive or personal information. Nevertheless, it is usually best to try and zero in on a common frame of reference as to what constitutes sensitive and personal information. In this section, you will try to do that by taking a look at the table header first, and then going over each of the columns:
Personal information can be classified as data that is known to you and a limited number of people within your social circle. Personal information is usually something that is private to you, but that you would be willing to share with close friends and family members. Examples of personal information can be your phone number, address, and e-mail address. The effects of having this information compromised and leaked will usually not cause significant physical or emotional harm to yourself or your family members. Instead, it may give rise to situations that will greatly inconvenience you.
What Is Sensitive Information?
Sensitive information is worth much more than personal information. Sensitive information is usually information that you will not share with anyone under most circumstances. Data of this type includes your passwords, Internet banking credentials (such as PIN codes), mobile phone number, Social Security number, or address. If sensitive information is compromised, then the effects may cause you either physical or emotional harm. This information should be protected all the time, regardless of whether it is in transit or in storage.
Caution How can the loss of sensitive information cause you physical or emotional harm? Consider losing your online banking credentials. An attacker can cause you immense financial (physical and emotional) harm by stealing all your money. A stalker that gets hold of your phone number or address can pose a grave threat to you or your family’s physical well being.
Analysis of Code
If we go back to the indirect attack that we discussed earlier in this chapter, it is evident that data kept in clear view on an SD Card is a significant risk and should be avoided at all costs. Data theft or exposure has been one of the leading causes of financial and reputational loss for corporations. But just because you’re writing an application for a single user of a smartphone does not mean you should treat data theft lightly. In the case of Proxim, this weakness of clear text data storage exists. Anyone who has access to the device’s SD Card will be able to copy personal information, such as names, addresses, phone numbers, and e-mail addresses.
We can trace the flaw in the original code to the point where we save the data. The data itself is not obscured or encrypted in any way. If we were to encrypt the data, then the personal information would still be safe. Let’s take a look at how we can implement encryption in our original Proxim code. Chapter 5 will cover public key infrastructure and encryption in depth; so for the purposes of this exercise, we will cover a very basic example of Advanced Encryption Standard (AES) encryption. Public Key encryption or Asymmetric encryption is a method of encrypting or obfuscating data by using two different types of keys. Each user has two keys, a public and a private one. His private key can only decrypt data that is encrypted by the public key. The key is called public because it is freely given away to other users. It is this key that other users will use to encrypt data.
Where to Implement Encryption
We will encrypt our data just before we save it to the SD Card. In this way, we never write the data to the SD Card in a format that can be read by anyone. An attacker that collects your encrypted data has to first use a password to decrypt the data before having access to it.
We will use AES to encrypt our data using a password or key. One key is required to both encrypt and decrypt the data. This is also known s symmetric key encryption. Unlike public key encryption, this key is the sole one used to both encrypt and decrypt data. This key will need to be stored securely because, if it is lost or compromised, an attacker can use it to decrypt the data. Listing 2-5 shows the encryption routine.
Listing 2-5. An Encryption Routine
privatestaticbyte[] encrypt(byte[] key, byte[] data){
SecretKeySpec sKeySpec = new SecretKeySpec(key,"AES");
Cipher cipher;
byte[] ciphertext = null;
try {
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, sKeySpec);
ciphertext = cipher.doFinal(data);
} catch (NoSuchAlgorithmException e) {
Log.e(TAG,"NoSuchAlgorithmException");
} catch (NoSuchPaddingException e) {
Log.e(TAG,"NoSuchPaddingException");
} catch (IllegalBlockSizeException e) {
Log.e(TAG,"IllegalBlockSizeException");
} catch (BadPaddingException e) {
Log.e(TAG,"BadPaddingException");
} catch (InvalidKeyException e) {
Log.e(TAG,"InvalidKeyException");
}
return ciphertext;
}
Let’s go through the code, section by section. The first bit of code initializes the SecretKeySpec class and creates a new instance of the Cipher class in preparation of generating an AES secret key:
SecretKeySpec sKeySpec = new SecretKeySpec(key,"AES");
Cipher cipher;
byte[] ciphertext = null;
The preceding code also initializes a byte array to store the ciphertext. The next bit of code prepares the Cipher class to use the AES algorithm:
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, sKeySpec);
The cipher.init() function initializes the Cipher object, so it can perform encryption using the generated secret key. The next line of code encrypts the plain text data and stores the encrypted contents in the ciphertext byte array:
ciphertext = cipher.doFinal(data);
In order for the preceding routine to work, it should always have an encryption key. It is important that we use the same key for the decryption routine, as well. Otherwise, it will fail. It is generally better to write your own key generator that will generate a random number–based key. This will make it harder for an attacker to guess than a normal password. For this exercise, I used the key-generation algorithm shown in Listing 2-6.
Listing 2-6. A Key-Generation Algorithm
publicstaticbyte[] generateKey(byte[] randomNumberSeed) {
SecretKey sKey = null;
try {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(randomNumberSeed);
keyGen.init(256,random);
sKey = keyGen.generateKey();
} catch (NoSuchAlgorithmException e) {
Log.e(TAG,"No such algorithm exception");
}
return sKey.getEncoded();
}
Now, let’s analyze the code. This pair of lines initializes the KeyGenerator class so it can generate AES-specific keys, and then initializes the device’s random-number generator so it can generate random numbers:
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
These random numbers are encoded using SHA1. SHA1, or Secure Hash Algorithm 1, is a cryptographic hashing function. The algorithm will operate on a piece of data that has an arbitrary length and will produce a short string that is of fixed size. If any piece of the data being hashed is changed, then the resulting hash will vary. This is an indication that a piece of data has been tampered with.
The next snippet of code uses the random-number seed provided to generate a 256-bit key using this random number:
random.setSeed(randomNumberSeed);
keyGen.init(256,random);
sKey = keyGen.generateKey();
Simply run the key-generation algorithm once and save the resulting key to use with the decryption routine.
When we examine the same Contact object in the SD Card, the contents appear garbled (see Figure 2-5) and unreadable by any casual snoopers or deliberate attackers.
Figure 2-5. The encrypted contents of the Contact object
Reworked Project 1
Our changes to the Proxim project mostly affect the saveController() method (see Listing 2-7).
Listing 2-7. The Reworked SaveController.java method
package net.zenconsult.android.controller;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import net.zenconsult.android.crypto.Crypto;
import net.zenconsult.android.model.Contact;
import net.zenconsult.android.model.Location;
import android.content.Context;
import android.os.Environment;
import android.util.Log;
public class SaveController {
private static final String TAG = "SaveController";
public static void saveContact(Context context, Contact contact) {
if (isReadWrite()) {
try {
File outputFile = new File(context.getExternalFilesDir
(null),contact.getFirstName());
FileOutputStream outputStream = new FileOutputStream
(outputFile);
byte[] key = Crypto.generateKey
("randomtext".getBytes());
outputStream.write(encrypt(key,contact.getBytes()));
outputStream.close();
} catch (FileNotFoundException e) {
Log.e(TAG,"File not found");
} catch (IOException e) {
Log.e(TAG,"IO Exception");
}
} else {
Log.e(TAG,"Error opening media card in read/write mode!");
}
}
public static void saveLocation(Context context, Location location) {
if (isReadWrite()) {
try {
File outputFile = new File(context.getExternalFilesDir
(null),location.getIdentifier());
FileOutputStream outputStream = new FileOutputStream
(outputFile);
byte[] key = Crypto.generateKey
("randomtext".getBytes());
outputStream.write(encrypt(key,location.getBytes()));
outputStream.close();
} catch (FileNotFoundException e) {
Log.e(TAG,"File not found");
} catch (IOException e) {
Log.e(TAG,"IO Exception");
}
} else {
Log.e(TAG,"Error opening media card in read/write mode!");
}
}
private static boolean isReadOnly() {
Log.e(TAG,Environment
.getExternalStorageState());
return Environment.MEDIA_MOUNTED_READ_ONLY.equals(Environment
.getExternalStorageState());
}
private static boolean isReadWrite() {
Log.e(TAG,Environment
.getExternalStorageState());
return Environment.MEDIA_MOUNTED.equals(Environment
.getExternalStorageState());
}
private static byte[] encrypt(byte[] key, byte[] data){
SecretKeySpec sKeySpec = new SecretKeySpec(key,"AES");
Cipher cipher;
byte[] ciphertext = null;
try {
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, sKeySpec);
ciphertext = cipher.doFinal(data);
} catch (NoSuchAlgorithmException e) {
Log.e(TAG,"NoSuchAlgorithmException");
} catch (NoSuchPaddingException e) {
Log.e(TAG,"NoSuchPaddingException");
} catch (IllegalBlockSizeException e) {
Log.e(TAG,"IllegalBlockSizeException");
} catch (BadPaddingException e) {
Log.e(TAG,"BadPaddingException");
} catch (InvalidKeyException e) {
Log.e(TAG,"InvalidKeyException");
}
return ciphertext;
}
}
Exercise
Summary
Storing plain text or other easily read data on mobile devices is something you should avoid doing at all costs. Even though your application itself might be written securely, an indirect attack that originates from a completely different area on the device can still collect and read sensitive or personal information written by your application. Follow the following basic steps during application design:
3.147.57.145