Created
April 21, 2015 06:53
-
-
Save z0lope0z/8b5dc3ad01b05936d839 to your computer and use it in GitHub Desktop.
ObscuredSharedPreferences.java by RightHandedMonkey
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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