MySecure: Exploring App Security



This application demonstrates some of the features and limitations of an Android app. It shows you how to backup your data, explore the file system of your device, activate a heartbeat service to report on location, and even how to wipe out data via a secret SMS message. The real goal is to illustrate how to create a security application. This is by no means a commercial product, but an educational example of various Android features.

So, the main activity, MySecure just starts various other activities:


/src/com.marakana/MySecure.java

Code:

package com.marakana;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Browser;
import android.provider.Contacts;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class MySecure extends Activity implements OnClickListener {
public static final String TAG = "MySecure";
Button buttonStartService, buttonStopService;
Button buttonPrefs, buttonBackup, buttonFileList, buttonAppList;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

// Find buttons
buttonStartService = (Button) findViewById(R.id.buttonStartService);
buttonStartService.setOnClickListener(this);

buttonStopService = (Button) findViewById(R.id.buttonStopService);
buttonStopService.setOnClickListener(this);

buttonPrefs = (Button) findViewById(R.id.buttonPrefs);
buttonPrefs.setOnClickListener(this);

buttonBackup = (Button) findViewById(R.id.buttonBackup);
buttonBackup.setOnClickListener(this);

buttonFileList = (Button) findViewById(R.id.buttonFileList);
buttonFileList.setOnClickListener(this);

buttonAppList = (Button) findViewById(R.id.buttonAppList);
buttonAppList.setOnClickListener(this);
}

@Override
public void onStop() {
super.onStop();
}

public void onClick(View v) {
switch (v.getId()) {
case R.id.buttonStartService:
startService(new Intent(this, HeartbeatService.class));
break;
case R.id.buttonStopService:
stopService(new Intent(this, HeartbeatService.class));
break;
case R.id.buttonPrefs:
startActivity(new Intent(this, Prefs.class));
break;
case R.id.buttonBackup:
Backup backup = new Backup(this);
backup.runBackup(Contacts.People.CONTENT_URI);
backup.runBackup(Settings.System.CONTENT_URI);
backup.runBackup(Browser.BOOKMARKS_URI);
backup.runDBBackup(HeartbeatDBHelper.TABLE);
break;
case R.id.buttonFileList:
startActivity(new Intent(this, FileList.class));
break;
case R.id.buttonAppList:
startActivity(new Intent(this, AppList.class));
break;
}
}

/** Initializes the menu. Called only once, first time user clicks on menu. **/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menuCallSupport:
startActivity(new Intent(Intent.ACTION_CALL, Uri
.parse("tel:1-800-555-1212")));
break;
}
return true;
}
}

Next is the HeartbeatService. Its job is to every so often send a "heartbeat" to twitter.com. It also figures out its location and includes it in the notification. This way, we can monitor where the device is physically located.

This service is started/stopped from MyTwitter activity, as well as at boot time by the HeartbeatStarter.

/src/com/marakana/HaeartbeatService.java

Code:

package com.marakana;

import winterwell.jtwitter.Twitter;
import android.app.Service;
import android.content.ContentValues;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.sqlite.SQLiteDatabase;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;

public class HeartbeatService extends Service implements LocationListener {
public static final String TAG = "HeartbeatService";
public static final long DELAY = 600000; // 10 min
Twitter twitter;
SQLiteDatabase db;
SharedPreferences prefs;
Handler handler = null;
Heartbeat heartbeat;
LocationManager locationManager;
Location location;
String bestProvider;

@Override
public void onCreate() {
super.onCreate();

prefs = PreferenceManager.getDefaultSharedPreferences(this);

// Register to get notified when preferences change
prefs
.registerOnSharedPreferenceChangeListener(new OnSharedPreferenceChangeListener() {
public void onSharedPreferenceChanged(
SharedPreferences sharedPreferences, String key) {
twitter = null;
Log.d(TAG, "Preferences Changed!");
}
});

// Open the database
HeartbeatDBHelper dbHelper = new HeartbeatDBHelper(this);
db = dbHelper.getWritableDatabase();

// Get location manager
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);

Log.d(TAG, "onCreate'd");
}

// Called when service is starting
@Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);

// Register for location updates
setupLocationUpdates();

// Setup heartbeats to run on handler
if (handler == null) {
handler = new Handler();
heartbeat = new Heartbeat();
handler.post(heartbeat);
}

Toast.makeText(this, "Heartbeat Started", Toast.LENGTH_LONG).show();
}

// Called when service is about to be stopped
@Override
public void onDestroy() {
super.onDestroy();
handler.removeCallbacks(heartbeat);
locationManager.removeUpdates(this);
handler = null;
twitter = null;
Toast.makeText(this, "Heartbeat Stopped", Toast.LENGTH_LONG).show();
}

@Override
public IBinder onBind(Intent i) {
return null;
}

// Lazy initialization of Twitter, useful when Prefs change
private Twitter getTwitter() {
if (twitter == null) {
String username = prefs.getString("username", "");
String password = prefs.getString("password", "");
twitter = new Twitter(username, password);
Log.d(TAG, String.format("getTwitter reinitilized with %s/%s", username,
password));
}
return twitter;
}

// The runnable that wakes up every so often and sends
// a heart beat to Twitter
class Heartbeat implements Runnable {
String msg;
ContentValues values = new ContentValues();

// Runs over and over again
public void run() {
// Log to DB as well
values.put(HeartbeatDBHelper.C_RECORDED_AT, System.currentTimeMillis());
values.put(HeartbeatDBHelper.C_LAT,
(location!=null)?location.getLatitude():0.0);
values.put(HeartbeatDBHelper.C_LONG,
(location!=null)?location.getLongitude():0.0);
db.insert(HeartbeatDBHelper.TABLE, null, values);

// Log to Twitter, if available
if (getTwitter().isValidLogin()) {
if(location!=null)
msg = String.format("HB from (%.5f,%.5f)", location.getLatitude(),
location.getLongitude());
else msg = "Location unknown";
getTwitter().updateStatus(msg);
Log.d(TAG, "Heartbeat sent: " + msg);
}

// Run this code again after DELAY
handler.postDelayed(this, DELAY);
}
}

public void onLocationChanged(Location location) {
this.location = location;
Log.d(TAG, "onLocationChanged with Location: " + location);
}

public void onProviderDisabled(String provider) {
//setupLocationUpdates();
}

public void onProviderEnabled(String provider) {
//setupLocationUpdates();
}

// Called to setup location updates
private void setupLocationUpdates() {
locationManager.removeUpdates(this);
bestProvider = locationManager.getBestProvider(new Criteria(), true);
location = locationManager.getLastKnownLocation(bestProvider);
locationManager.requestLocationUpdates(bestProvider, DELAY, 100, this);
Log.d(TAG, "setupLocationUpdates with Location: " + location);
}

public void onStatusChanged(String provider, int status, Bundle extras) {
}
}

Because we want to start the service at boot time, we implement the broadcast receiver to do so. This receiver is set to get notified when the system gets booted via RECEIVE_BOOT_COMPLETED broadcast.

/src/com/marakana/HeartbeatStarter.java

Code:

package com.marakana;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class HeartbeatStarter extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
// Start the HeartbeatService
context.startService( new Intent(context, HeartbeatService.class) );
}

}

In order to have the heartbeat work and be able to connect to twitter.com, we need a way to store username/password for it. Android supports an elegant way to store preferences. This is the activity part:

/src/com/marakana/Prefs.java
Code:

package com.marakana;

import android.os.Bundle;
import android.preference.PreferenceActivity;

public class Prefs extends PreferenceActivity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.addPreferencesFromResource(R.xml.prefs);
}

}

And the corresponding XML file is:

/res/xml/prefs.xml
Code:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="https://schemas.android.com/apk/res/android">
<EditTextPreference android:title="User name"
android:key="username" android:summary="Twitter user name" />
<EditTextPreference android:title="Password"
android:key="password" android:summary="Your Twitter password" />
</PreferenceScreen>

The heartbeat service stores the hearbeats into a database as well. For an Android app to use a database, it needs a helper to help create it. This is handled in this DB helper:

/src/com/marakana/HeartbeatDBHelper.java
Code:

package com.marakana;

import android.content.Context;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
import android.util.Log;

// Helps open the heartbeat database
public class HeartbeatDBHelper extends SQLiteOpenHelper {
public static final String TAG = "HeartbeatDBHelper";
public static final String DB_NAME = "heartbeat.db";
public static final int DB_VERSION = 5;
public static final String TABLE = "heartbeats";
public static final String C_RECORDED_AT = "recorded_at";
public static final String C_LAT = "latitude";
public static final String C_LONG = "longitude";
Context context;

public HeartbeatDBHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
this.context = context;
}

// Called only once, when database gets created first time
@Override
public void onCreate(SQLiteDatabase db) {
String sql = String.format(
"create table %s (%s integer primary key autoincrement,"
+ "%s integer, %s float, %s float);", TABLE, BaseColumns._ID,
C_RECORDED_AT, C_LAT, C_LONG);
Log.d(TAG, sql);
db.execSQL(sql);
}

// This is where you upgrade your schema
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
try {
String sql = context.getResources().getString(R.string.sqlDropTable);
db.execSQL(sql);
} catch (SQLException e) {
}
onCreate(db);
}

}

Next is a simple implementation of the backup feature. We simply backup whatever we can (have permissions for), such as our app files, SDCard files, Contacts, and Bookmarks.

/src/com/marakana/Backup.java
Code:

package com.marakana;

import java.io.FileOutputStream;
import java.io.IOException;

import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;

public class Backup {
Context context;
ContentResolver cr;
HeartbeatDBHelper dbHelper;
SQLiteDatabase db;

public Backup(Context context) {
this.context = context;

// Get content resolver
cr = context.getContentResolver();

// Open the database
dbHelper = new HeartbeatDBHelper(context);
db = dbHelper.getReadableDatabase();
}

// Runs the backup of a content provider into a text file
public int runBackup(Uri uri) {
int count = 0;
String file = uri.getHost() +"-"+ System.currentTimeMillis();

Cursor cursor = cr.query(uri, null, null, null, null);
count = cursorToCSV(cursor, file);
cursor.close();

String msg = String.format("Backed up %d records to %s file", count, file);
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();

return count;
}

// Backs up a database table
public int runDBBackup(String table) {
int count = 0;
String file = table +"-"+ System.currentTimeMillis();

// Query the database table
Cursor cursor = db.query(table, null, null, null, null, null, null);
count = cursorToCSV(cursor, file);
cursor.close();

String msg = String.format("Backed up %d records to %s file", count, file);
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();

return count;
}

// Takes a cursor, iterates over it and, and saves CSV to file
private int cursorToCSV(Cursor cursor, String file) {
StringBuffer out = new StringBuffer();
int count=0;

// loop thru all the records
while (cursor.moveToNext()) {
// loop thru all the columns
for (int i = 0; i < cursor.getColumnCount(); i++) {
String column = cursor.getString(i);
if (i != 0)
out.append(",");
out.append(column); // adds a column
}
out.append("\n");
count++;
}

// save to a file
try {
FileOutputStream outStream = context.openFileOutput(file,
Context.MODE_PRIVATE);
outStream.write(out.toString().getBytes());
outStream.close();
Log.d("Backup", out.toString());
} catch (IOException e) {
Log.d("Backup", "Error writing CSV to file: "+file);
e.printStackTrace();
}

return count;
}
}

Here's another feature, a broadcast receiver that listens to a special kind of SMS message to notify the user that we know the phone is stolen.

/src/com/marakana/RecoveryReceiver.java
Code:

package com.marakana;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telephony.gsm.SmsMessage;
import android.util.Log;
import android.widget.Toast;

public class RecoveryReceiver extends BroadcastReceiver {

// Triggered by an incoming SMS
@Override
public void onReceive(Context context, Intent intent) {
Bundle bundle = intent.getExtras();
SmsMessage[] msgs = null;
String from, body;
Log.d("RecoveryReceiver", "onReceive");
if (bundle != null) {
// ---retrieve the SMS message received---
Object[] pdus = (Object[]) bundle.get("pdus");
msgs = new SmsMessage[pdus.length];
for (int i = 0; i < msgs.length; i++) {
msgs[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
from = msgs[i].getOriginatingAddress();
body = msgs[i].getMessageBody().toString();

// Is it the recovery message
if("It rains in Spain".equals(body.trim())) {
// Delete the phone data
Toast.makeText(context, "PHONE STOLEN!", Toast.LENGTH_LONG).show();
Log.d("RecoveryReceiver", "PHONE STOLEN!");
} else {
Log.d("RecoveryReceiver", "Ignored");
}
}
}
}

}

Finally, the File browser activity that shows the list of all the files that we can see. It even color-codes the files based on our permissions.

/src/com/marakana/FileList.java
Code:

package com.marakana;

import java.io.File;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast;
import android.widget.AdapterView.OnItemClickListener;

public class FileList extends Activity implements OnItemClickListener, OnClickListener {
ListView listFiles;
FileAdapter adapter;
File[] files; // full path to the file
File file; // current file
File SDCARD = new File("/sdcard/");
File ROOT = new File("/");
File HOME;

Button buttonUp, buttonSDCard, buttonHome, buttonRoot;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.filelist);

HOME = this.getFilesDir();

// Find buttons
buttonUp = (Button) findViewById(R.id.buttonUp);
buttonUp.setOnClickListener(this);

buttonSDCard = (Button) findViewById(R.id.buttonSDCard);
buttonSDCard.setOnClickListener(this);

buttonHome = (Button) findViewById(R.id.buttonHome);
buttonHome.setOnClickListener(this);

buttonRoot = (Button) findViewById(R.id.buttonRoot);
buttonRoot.setOnClickListener(this);

// Find list view and set it up
listFiles = (ListView) findViewById(R.id.listFiles);
listFiles.setOnItemClickListener(this);

// Show / initially
file = new File("/");
updateList(file);
}

public void onItemClick(AdapterView<?> listView, View row, int position,
long id) {
// Get the item that was clicked on
file = adapter.getItem(position);

// Check if it's a directory
if (file.isDirectory() && file.canRead()) {
updateList(file);
} else {
Toast
.makeText(this, "Not directory or no permissions", Toast.LENGTH_LONG)
.show();
}
}

private void updateList(File parent) {
// Update the list of files for that directory
files = parent.listFiles();

// Update the adapter
adapter = new FileAdapter(this, files);
listFiles.setAdapter(adapter);
}

public void onClick(View button) {
switch(button.getId()) {
case R.id.buttonUp:
file = (file.getParentFile()!=null)?file.getParentFile():file;
updateList(file);
break;
case R.id.buttonSDCard:
updateList(SDCARD);
break;
case R.id.buttonHome:
updateList(HOME);
break;
case R.id.buttonRoot:
updateList(ROOT);
}
}

}

And the corresponding adapter to help us manage the file list:

/src/com/marakana/FileAdapter.java
Code:

package com.marakana;

import java.io.File;

import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

public class FileAdapter extends ArrayAdapter<File> {
Context context;
File[] files;

public FileAdapter(Context context, File[] files) {
super(context, -1, files);
this.context = context;
this.files = files;
}

// Called each time adapter is setting a single row with its data
@Override
public View getView(int position, View row, ViewGroup parent) {
// Inflate new row if not already initialized
if (row == null) {
LayoutInflater inflater = ((Activity) context).getLayoutInflater();
row = inflater.inflate(R.layout.row, null);
}

// Get current file
File file = files[position];

// Update the row
TextView textFileName = (TextView) row.findViewById(R.id.textFileName);
if (file.isDirectory()) {
textFileName.setText(file.getPath() + " [dir]");
textFileName.setTextColor(Color.BLUE);
} else {
textFileName.setText(String
.format("%s [%d bytes]", file.getPath(), file.length()));
if (file.canRead())
textFileName.setTextColor(Color.GREEN);
}

return row;
}

}

Similarly, we have the activity that show us the list of all the applications (packages) on the system. This is useful to see how you can introspect apps installed on the platform via PackageManager:

/src/com/marakana/AppList.java
Code:

package com.marakana;

import java.util.List;

import android.app.Activity;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.ListView;

public class AppList extends Activity {
ListView listApps;
PackageManager pm;
PackageInfo[] apps;
AppAdapter adapter;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.applist);

// Find views
listApps = (ListView) findViewById(R.id.listApps); // View

// Get package manager
pm = this.getPackageManager();
List<PackageInfo> appsList = pm.getInstalledPackages(pm.GET_META_DATA);
apps = new PackageInfo[appsList.size()];
apps = appsList.toArray(apps);// Model

// Setup adapter
adapter = new AppAdapter(this, apps); // Controller
listApps.setAdapter(adapter);
}
}

And its corresponding AppAdapter to help manage the actual list of apps:

/src/com/marakana/AppAdapter.java
Code:

package com.marakana;

import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.graphics.Color;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

public class AppAdapter extends ArrayAdapter<PackageInfo> {
Context context;
PackageInfo[] apps;
String text;

public AppAdapter(Context context, PackageInfo[] apps) {
super(context, -1, apps);
this.context = context;
this.apps = apps;
}

// Called each time adapter is setting a single row with its data
@Override
public View getView(int position, View row, ViewGroup parent) {
// Inflate new row if not already initialized
if (row == null) {
LayoutInflater inflater = ((Activity) context).getLayoutInflater();
row = inflater.inflate(R.layout.row, null);
}

// Get current app
PackageInfo app = apps[position];

// Update the row
TextView textFileName = (TextView) row.findViewById(R.id.textFileName);

// Quick stats on the app (package)
text = String.format("%s [%d/%s]",app.packageName,
app.versionCode, app.versionName);
textFileName.setText(text);
textFileName.setTextColor(Color.BLUE);

Log.d("AppAdapter", app.toString());
return row;
}

}

There are some other files in this app, such as numerous layouts and such, but I'll leave that to you to download as part of the complete zip file.

Output

Here are some of the screens for your amusement:





Source

https://www.protechtraining.com/static/tutorials/MySecure.zip
https://www.protechtraining.com/static/tutorials/MySecure.apk

Published April 13, 2010