Click here to Skip to main content
15,867,686 members
Articles / Mobile Apps / Android

Encryption Wrapper for Android SharedPreferences

Rate me:
Please Sign up or sign in to vote.
5.00/5 (14 votes)
27 Mar 2013Apache5 min read 63.6K   3.4K   34   8
This article explains why and how you should protect your app's settings from prying eyes

Introduction

This article presents a wrapper class for Android's SharedPreferences interface, which adds a layer of encryption to the persistent storage and retrieval of sensitive key-value pairs of primitive data types.

Why should you care about this as an Android developer? Read on...

Background

Android's SharedPreferences interface provides a general framework that allows you to access and modify key-value pairs of primitive data types (booleans, numbers, strings, and more). This data persists across user sessions, even if your application is killed. For more information, see the data storage developer guide and the Activity class' reference documentation.

By default, Android stores this data in an unencrypted XML file within the app's directory on the device's filesystem, with permissions that allow only the app to access this file. This is part of the concept known as "application sandboxing". So the data is private and protected, right? Well, not quite.

If the Android device is rooted, other apps (with root privileges) can read/download/modify this file (as well as the entire filesystem). Worse still, even if the device is unrooted but attackers gain physical access to it, they might be able to download all the data from it, for example with the Android Debug Bridge (ADB). See the AOSP security tech info for more information.

So what can you do? Encrypt the data! This way, even if attackers gain access to it, it will remain unreadable and unmodifiable. The class presented below provides an easy and generic solution for this, based on Google's recommendations in the Android Developers Blog.

Using the code

As you may already know, there are 3 methods to initialize a SharedPreferences object:

The class in this article accepts a Context object and uses the 3rd approach internally, so instead of the above you should do the following (where this is your app's Activity, Service, etc.):

Java
SharedPreferences prefs = SecurePreferences(this);

For now, this class does not have a constructor which accepts an existing SharedPreferences object for wrapping. The reasons for this safety measure are specified in the "Attention" section below, but future versions of this article may provide additional constructors.

By the way, the constructor also initializes the encryption/decryption key in memory.

Java
public class SecurePreferences implements SharedPreferences {
    private static SharedPreferences sFile;
    private static byte[] sKey;
    
    /**
     * Constructor.
     *
     * @param context the caller's context
     */
    public SecurePreferences(Context context) {
        // Proxy design pattern
        if (SecurePreferences.sFile == null) {
            SecurePreferences.sFile = PreferenceManager.getDefaultSharedPreferences(context);
        }
        // Initialize encryption/decryption key
        try {
            final String key = SecurePreferences.generateAesKeyName(context);
            String value = SecurePreferences.sFile.getString(key, null);
            if (value == null) {
                value = SecurePreferences.generateAesKeyValue();
                SecurePreferences.sFile.edit().putString(key, value).commit();
            }
            SecurePreferences.sKey = SecurePreferences.decode(value);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }
    ...
}

That's it! Other than that, you use this class just like a regular SharedPreferences object. The only difference is that this class transparently encrypts and decrypts the keys and the values that you provide.

For example, here is how you can get and set a string:

Java
String value = prefs.getString("myKey", "defaultValue");
prefs.edit().putString("myKey", "myValue").commit();

And here is the implementation of these calls:

Java
@Override
public String getString(String key, String defaultValue) {
    final String encryptedValue =
            SecurePreferences.sFile.getString(SecurePreferences.encrypt(key), null);
    return (encryptedValue != null) ? SecurePreferences.decrypt(encryptedValue) : defaultValue;
}
@Override
public SharedPreferences.Editor putString(String key, String value) {
    mEditor.putString(SecurePreferences.encrypt(key), SecurePreferences.encrypt(value));
    return this;
}

Lastly, FYI:

  • This class requires API level 8 (Android 2.2, a.k.a. "Froyo") or greater
  • The example above shows strings, but all the other data types of the SharedPreferences interface are supported as well: boolean, float, int, long, and Set<String>
  • null and empty string values are not encrypted

Attention

The "Background" section above explained the "application sandboxing" concept, which provides some privacy by default. It's important to understand the details of this default behavior, so it won't be changed inadvertently and weaken your app's security:

  1. The SharedPreferences object instance should be initialized with the MODE_PRIVATE flag
  2. The location of the SharedPreferences object's persistent XML file should be in the device's internal storage, rather than on an SD card, since permissions aren't enforced on external storage

Also, the class presented above provides important - but nevertheless imperfect - protection against simple attacks by casual snoopers. It is crucial to remember that even encrypted data may still be susceptible to attacks by advanced attackers, especially on rooted or stolen devices!

As written by Michael Burton (a.k.a. emmby) in Stack Overflow:

Any attacker that has access to your preferences file is fairly likely to also have access to your application's binary, and therefore the keys to unencrypt the password.

Further Improvements

A suggestion from the Android security tips web page, in case your sensitive data is user credentials:

Where possible, username and password should not be stored on the device. Instead, perform initial authentication using the username and password supplied by the user, and then use a short-lived, service-specific authorization token.

And another suggestion from the same web page, for general sensitive data:

To provide additional protection for sensitive data, you might choose to encrypt local files using a key that is not directly accessible to the application. For example, a key can be placed in a KeyStore and protected with a user password that is not stored on the device.

And yet another suggestion from the Android Developers Blog:

If your app needs additional encryption, a recommended approach is to require a passphase or PIN to access your application. This passphrase could be fed into PBKDF2 to generate the encryption key.

In other words, an even safer approach would be to have a secret PIN/passphrase/password which is not stored on the device at all. Either the user provides it, or a remote and secure server (which would require internet connectivity and deeper security evaluation). This secret would be converted by the app to the encryption/decryption key.

This way, even if the device falls into the wrong hands, the only way to hack the data would be with brute-force attacks.

Points of Interest

This article dealt with Android SharedPreferences. If you want to encrypt SQLite databases, you can check out SQLCipher.

History 

  • 27 Mar 2013 - Added 2 new samples (integration with Android's PreferenceActivity and PreferenceFragment), added a few @TargetApi annotations in SecurePreferences.java, and fixed the getAll() method to ignore decryption failures in unencrypted key/value pairs
  • 25 Feb 2013 - Initial revision

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
United States United States
I'm a veteran software engineer. My professional focus is currently test automation for next-generation server network controllers. I also work a lot with Linux and Android, and dabble with wireless technologies in my spare time.

Comments and Discussions

 
GeneralMy vote of 5 Pin
baranfun28-Aug-17 19:45
baranfun28-Aug-17 19:45 
Questionpreferences across multiple Activities Pin
Marc Ilgen19-Sep-13 11:59
Marc Ilgen19-Sep-13 11:59 
QuestionTypo Pin
BillJones1222111-Sep-13 17:47
BillJones1222111-Sep-13 17:47 
NewsLink to Github version of code Pin
scottyab17-Jul-13 22:09
scottyab17-Jul-13 22:09 
QuestionVery informative article - and a question Pin
Giannakakis Kostas27-Mar-13 21:56
professionalGiannakakis Kostas27-Mar-13 21:56 
QuestionQuick question Pin
Tomas Brennan20-Mar-13 5:33
Tomas Brennan20-Mar-13 5:33 
Can this be used in this instance of code example, such as a
Java
PreferenceActivity
:

Java
public class PrefsMyAct extends PreferenceActivity{

	private SharedPreferences mSecPrefs = null;
	private CheckBoxPreference mChkBoxKey1;
	private CheckBoxPreference mChkBoxKey2;
	private Context mContext = null;

	@Override
	public void onCreate(Bundle savedInstanceState){
		super.onCreate(savedInstanceState);
		addPreferencesFromResource(R.xml.pref_myact);
		this.mContext = this.getApplicationContext();
		this.mSecPrefs = new SecurePreferences(this.mContext);
		this.mChkBoxKey1 = (CheckBoxPreference)findPreference(getString(R.string.keyChkBox1));
		if (this.mChkBoxKey1 != null){
			this.mChkBoxKey1.setOnPreferenceChangeListener(new OnPreferenceChangeListener(){

				@Override
				public boolean onPreferenceChange(Preference preference, Object newValue) {
					boolean blnKeyVal = Boolean.getBoolean(newValue.toString());
					return mSecPrefs.edit().putBoolean(getString(R.string.keyChkBox1), blnKeyVal).commit();
				}
				
			});
		}
		this.mChkBoxKey2 = (CheckBoxPreference)findPreference(getString(R.string.keyChkBox2));
		if (this.mChkBoxKey2 != null){
			this.mChkBoxKey2.setOnPreferenceChangeListener(new OnPreferenceChangeListener(){

				@Override
				public boolean onPreferenceChange(Preference preference, Object newValue) {
					boolean blnKeyVal = Boolean.getBoolean(newValue.toString());					
					return mSecPrefs.edit().putBoolean(getString(R.string.keyChkBox2), blnKeyVal).commit();
				}
				
			});
		}
	}
}


I find that it does not actually encrypt more than one key, in fact, if that implemented
Java
OnSharedPreferenceChangeListener
interface, it shows the key as plain-text and not encrypted. Even navigating to the app's 'shared_prefs' directory on a rooted handset shows the keys are not encrypted.

What am I doing wrong?

Cheers for your time and thanks for publishing the article Smile | :)
SuggestionRe: Quick question Pin
danabr27-Mar-13 4:57
danabr27-Mar-13 4:57 
GeneralRe: Quick question Pin
Tomas Brennan27-Mar-13 6:39
Tomas Brennan27-Mar-13 6:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.