What are Shared Preferences for?
Shared Preferences on Android are great for keeping information that you use in an app. But this information is all stored in plain text. If a user has a rooted device, then they can simply go in and examine what you have in your preferences under /data/data/com.package/shared_prefs/AppPrefs.xml.
The data is all right there in plain view:
AppPrefs.xml
='1.0'='utf-8'='yes'
<map>
<string name="UserName">JoeBlow</string>
<int name="UserNum" value="2" />
<string name="auth_modified_date">2014-04-21 16:15:32</string>
<string name="Password">pass123</string>
</map>
So if you plan on storing passwords or other data that you don't want people to have easy access to, that is obviously no good. So to help secure the data in your shared Preferences, use the ObscuredSharedPreferences.java code here.
The encrypted data will look like this:
AppPrefs.xml
='1.0'='utf-8'='yes'
<map>
<string name="UserName">vbBchG+mE+/QGg+3+YuDW9MEI16hC7</string>
<string name="UserNum">PK8XONc4=</string>
<string name="auth_modified_date">C441dQ1G7L4=</string>
<string name="Password">Fx8kG7rWXqQ=</string>
</map>
How to Make Your Shared Preferences Encrypted
Thanks to emmby at http://stackoverflow.com/questions/785973/what-is-the-most-appropriate-way-to-store-user-settings-in-android-application/6393502#6393502. Simply wrap your own SharedPreferences
object in this one, and any data you read/write will be automatically encrypted and decrypted. This is a drop in replacement for SharedPreferences
and includes some additional functionality over the code in the StackOverflow post. Usage:
ObscuredSharedPreferences prefs = ObscuredSharedPreferences.getPrefs
(this, MY_APP_NAME, Context.MODE_PRIVATE);
prefs.edit().putString("foo","bar").commit();
prefs.getString("foo", null);
Additional Features
- This code has built-in logic to handle the case where the information was not previously encrypted such as when you upgrade your existing app to this code.
- The secret key is not hard coded, but is generated at runtime, so that a compromised device only affects that individual device.
The code can be retrieved for download here at the WorxForUs github repository for ObscuredSharedPreferences.
The Code - ObscuredSharedPreferences.java
package com.worxforus.android;
import java.util.Map;
import java.util.Set;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import com.worxforus.Base64Support;
import android.content.Context;
import android.content.SharedPreferences;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.util.Log;
public class ObscuredSharedPreferences implements SharedPreferences {
protected static final String UTF8 = "UTF-8";
private static char[] SEKRIT=null;
protected SharedPreferences delegate;
protected Context context;
private static ObscuredSharedPreferences prefs = null;
public static boolean decryptionErrorFlag = false;
public ObscuredSharedPreferences(Context context, SharedPreferences delegate) {
this.delegate = delegate;
this.context = context;
SEKRIT = Settings.Secure.ANDROID_ID.toCharArray();
}
public static void setNewKey(String key) {
SEKRIT = key.toCharArray();
}
public synchronized static
ObscuredSharedPreferences getPrefs(Context c, String appName, int contextMode) {
if (prefs == null) {
prefs = new ObscuredSharedPreferences(
c.getApplicationContext(),
c.getApplicationContext().getSharedPreferences(appName, contextMode) );
}
return prefs;
}
public class Editor implements SharedPreferences.Editor {
protected SharedPreferences.Editor delegate;
public Editor() {
this.delegate = ObscuredSharedPreferences.this.delegate.edit();
}
@Override
public Editor putBoolean(String key, boolean value) {
delegate.putString(key, encrypt(Boolean.toString(value)));
return this;
}
@Override
public Editor putFloat(String key, float value) {
delegate.putString(key, encrypt(Float.toString(value)));
return this;
}
@Override
public Editor putInt(String key, int value) {
delegate.putString(key, encrypt(Integer.toString(value)));
return this;
}
@Override
public Editor putLong(String key, long value) {
delegate.putString(key, encrypt(Long.toString(value)));
return this;
}
@Override
public Editor putString(String key, String value) {
delegate.putString(key, encrypt(value));
return this;
}
@Override
public void apply() {
delegate.commit();
}
@Override
public Editor clear() {
delegate.clear();
return this;
}
@Override
public boolean commit() {
return delegate.commit();
}
@Override
public Editor remove(String s) {
delegate.remove(s);
return this;
}
@Override
public android.content.SharedPreferences.Editor putStringSet
(String key, Set<String> values) {
throw new RuntimeException
("This class does not work with String Sets.");
}
}
public Editor edit() {
return new Editor();
}
@Override
public Map<String, ?> getAll() {
throw new UnsupportedOperationException();
}
@Override
public boolean getBoolean(String key, boolean defValue) {
String v;
try {
v = delegate.getString(key, null);
} catch (ClassCastException e) {
return delegate.getBoolean(key, defValue);
}
return v!=null ? Boolean.parseBoolean(decrypt(v)) : defValue;
}
@Override
public float getFloat(String key, float defValue) {
String v;
try {
v = delegate.getString(key, null);
} catch (ClassCastException e) {
return delegate.getFloat(key, defValue);
}
try {
return Float.parseFloat(decrypt(v));
} catch (NumberFormatException e) {
decryptionErrorFlag = true;
Log.e(this.getClass().getName(), "Warning,
could not decrypt the value. Possible incorrect key. "+e.getMessage());
}
return defValue;
}
@Override
public int getInt(String key, int defValue) {
String v;
try {
v = delegate.getString(key, null);
} catch (ClassCastException e) {
return delegate.getInt(key, defValue);
}
try {
return Integer.parseInt(decrypt(v));
} catch (NumberFormatException e) {
decryptionErrorFlag = true;
Log.e(this.getClass().getName(), "Warning,
could not decrypt the value. Possible incorrect key. "+e.getMessage());
}
return defValue;
}
@Override
public long getLong(String key, long defValue) {
String v;
try {
v = delegate.getString(key, null);
} catch (ClassCastException e) {
return delegate.getLong(key, defValue);
}
try {
return Long.parseLong(decrypt(v));
} catch (NumberFormatException e) {
decryptionErrorFlag = true;
Log.e(this.getClass().getName(), "Warning,
could not decrypt the value. Possible incorrect key. "+e.getMessage());
}
return defValue;
}
@Override
public String getString(String key, String defValue) {
final String v = delegate.getString(key, null);
return v != null ? decrypt(v) : defValue;
}
@Override
public boolean contains(String s) {
return delegate.contains(s);
}
@Override
public void registerOnSharedPreferenceChangeListener
(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
delegate.registerOnSharedPreferenceChangeListener
(onSharedPreferenceChangeListener);
}
@Override
public void unregisterOnSharedPreferenceChangeListener
(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
delegate.unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
}
@Override
public Set<String> getStringSet(String key, Set<String> defValues) {
throw new RuntimeException("This class does not work with String Sets.");
}
protected String encrypt( String value ) {
try {
final byte[] bytes = value!=null ? value.getBytes(UTF8) : new byte[0];
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
SecretKey key = keyFactory.generateSecret(new PBEKeySpec(SEKRIT));
Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES");
pbeCipher.init(Cipher.ENCRYPT_MODE, key, new PBEParameterSpec
(Settings.Secure.getString(context.getContentResolver(),
Secure.ANDROID_ID).getBytes(UTF8), 20));
return new String(Base64Support.encode(pbeCipher.doFinal(bytes),
Base64Support.NO_WRAP),UTF8);
} catch( Exception e ) {
throw new RuntimeException(e);
}
}
protected String decrypt(String value){
try {
final byte[] bytes = value!=null ?
Base64Support.decode(value,Base64Support.DEFAULT) : new byte[0];
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
SecretKey key = keyFactory.generateSecret(new PBEKeySpec(SEKRIT));
Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES");
pbeCipher.init(Cipher.DECRYPT_MODE, key, new PBEParameterSpec
(Settings.Secure.getString(context.getContentResolver(),
Secure.ANDROID_ID).getBytes(UTF8), 20));
return new String(pbeCipher.doFinal(bytes),UTF8);
} catch( Exception e) {
Log.e(this.getClass().getName(), "Warning, could not decrypt the value.
It may be stored in plaintext. "+e.getMessage());
return value;
}
}
}
Compile It
To compile this, you will also need the Base64
library. This was introduced in Android in API level 8, so earlier devices will not have the package. Include in your project the WorxForUs Base64Support package here.
Related Work
Please check out the WorxForUs library of Android solutions. The introduction is here and the framework can be found here. It includes other useful nuggets such as database access helpers for correctly handling multi-threaded environments and helpers for network access for cookies, network retry and other useful features.