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.):
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.
public class SecurePreferences implements SharedPreferences {
private static SharedPreferences sFile;
private static byte[] sKey;
public SecurePreferences(Context context) {
if (SecurePreferences.sFile == null) {
SecurePreferences.sFile = PreferenceManager.getDefaultSharedPreferences(context);
}
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:
String value = prefs.getString("myKey", "defaultValue");
prefs.edit().putString("myKey", "myValue").commit();
And here is the implementation of these calls:
@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:
- The SharedPreferences object instance should be initialized with the MODE_PRIVATE flag
- 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