Skip to content

Instantly share code, notes, and snippets.

@z0lope0z
Created April 21, 2015 06:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save z0lope0z/8b5dc3ad01b05936d839 to your computer and use it in GitHub Desktop.
Save z0lope0z/8b5dc3ad01b05936d839 to your computer and use it in GitHub Desktop.
ObscuredSharedPreferences.java by RightHandedMonkey
import java.io.UnsupportedEncodingException;
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;
/**
* Taken from https://github.com/RightHandedMonkey/WorxForUs_Library/blob/master/src/com/worxforus/android/ObscuredSharedPreferences.java
* Thanks to emmby at http://stackoverflow.com/questions/785973/what-is-the-most-appropriate-way-to-store-user-settings-in-android-application/6393502#6393502
*
* Documentation: http://right-handed-monkey.blogspot.com/2014/04/obscured-shared-preferences-for-android.html
* This class has the following additions over the original:
* additional logic for handling the case for when the preferences were not originally encrypted, but now are.
* The secret key is no longer hard coded, but defined at runtime based on the individual device.
* The benefit is that if one device is compromised, it now only affects that device.
*
* Simply replace your own SharedPreferences object in this one, and any data you read/write will be automatically encrypted and decrypted.
*
* Updated usage:
* ObscuredSharedPreferences prefs = ObscuredSharedPreferences.getPrefs(this, MY_APP_NAME, Context.MODE_PRIVATE);
* //to get data
* prefs.getString("foo", null);
* //to store data
* prefs.edit().putString("foo","bar").commit();
*/
public class ObscuredSharedPreferences implements SharedPreferences {
protected static final String UTF8 = "UTF-8";
//this key is defined at runtime based on ANDROID_ID which is supposed to last the life of the device
private static char[] SEKRIT=null;
private static byte[] SALT=null;
private static char[] backup_secret=null;
private static byte[] backup_salt=null;
protected SharedPreferences delegate;
protected Context context;
private static ObscuredSharedPreferences prefs = null;
//Set to true if a decryption error was detected
//in the case of float, int, and long we can tell if there was a parse error
//this does not detect an error in strings or boolean - that requires more sophisticated checks
public static boolean decryptionErrorFlag = false;
/**
* Constructor
* @param context
* @param delegate - SharedPreferences object from the system
*/
public ObscuredSharedPreferences(Context context, SharedPreferences delegate) {
this.delegate = delegate;
this.context = context;
//updated thanks to help from bkhall on github
ObscuredSharedPreferences.setNewKey(Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID));
ObscuredSharedPreferences.setNewSalt(Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID));
}
/**
* Only used to change to a new key during runtime.
* If you don't want to use the default per-device key for example
* @param key
*/
public static void setNewKey(String key) {
SEKRIT = key.toCharArray();
}
/**
* Only used to change to a new salt during runtime.
* If you don't want to use the default per-device code for example
* @param salt - this must be a string in UT
*/
public static void setNewSalt(String salt) {
try {
SALT = salt.getBytes(UTF8);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Accessor to grab the preferences in a singleton. This stores the reference in a singleton so it can be accessed repeatedly with
* no performance penalty
* @param c - the context used to access the preferences.
* @param appName - domain the shared preferences should be stored under
* @param contextMode - Typically Context.MODE_PRIVATE
* @return
*/
public synchronized static ObscuredSharedPreferences getPrefs(Context c, String appName, int contextMode) {
if (prefs == null) {
//make sure to use application context since preferences live outside an Activity
//use for objects that have global scope like: prefs or starting services
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() {
//to maintain compatibility with android level 7
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(); // left as an exercise to the reader
}
@Override
public boolean getBoolean(String key, boolean defValue) {
//if these weren't encrypted, then it won't be a string
String v;
try {
v = delegate.getString(key, null);
} catch (ClassCastException e) {
return delegate.getBoolean(key, defValue);
}
//Boolean string values should be 'true' or 'false'
//Boolean.parseBoolean does not throw a format exception, so check manually
String parsed = decrypt(v);
if (!checkBooleanString(parsed) ) {
//could not decrypt the Boolean. Maybe the wrong key was used.
decryptionErrorFlag = true;
Log.e(this.getClass().getName(), "Warning, could not decrypt the value. Possible incorrect key used.");
}
return v!=null ? Boolean.parseBoolean(parsed) : defValue;
}
/**
* This function checks if a valid string is received on a request for a Boolean object
* @param str
* @return
*/
private boolean checkBooleanString(String str) {
return ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str));
}
@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) {
//could not decrypt the number. Maybe we are using the wrong key?
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) {
//could not decrypt the number. Maybe we are using the wrong key?
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) {
//could not decrypt the number. Maybe we are using the wrong key?
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.");
}
/**
* Push key allows you to hold the current key being used into a holding location so that it can be retrieved later
* The use case is for when you need to load a new key, but still want to restore the old one.
*/
public static void pushKey() {
backup_secret = SEKRIT;
}
/**
* This takes the key previously saved by pushKey() and activates it as the current decryption key
*/
public static void popKey() {
SEKRIT = backup_secret;
}
/**
* pushSalt() allows you to hold the current salt being used into a holding location so that it can be retrieved later
* The use case is for when you need to load a new salt, but still want to restore the old one.
*/
public static void pushSalt() {
backup_salt = SALT;
}
/**
* This takes the value previously saved by pushSalt() and activates it as the current salt
*/
public static void popSalt() {
SALT = backup_salt;
}
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(SALT, 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(SALT, 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;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment