From c4a2699ce06859517d44d59820068741a1214798 Mon Sep 17 00:00:00 2001 From: Kevin Whitaker Date: Mon, 16 Nov 2015 21:17:50 -0500 Subject: [PATCH] Add in a class that should handle storing some preference data encrypted. --- .../login/ObscuredSharedPreferences.java | 349 ++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 app/src/main/java/com/magnatune/eyecreate/companionformagnatune/login/ObscuredSharedPreferences.java diff --git a/app/src/main/java/com/magnatune/eyecreate/companionformagnatune/login/ObscuredSharedPreferences.java b/app/src/main/java/com/magnatune/eyecreate/companionformagnatune/login/ObscuredSharedPreferences.java new file mode 100644 index 0000000..ef426f5 --- /dev/null +++ b/app/src/main/java/com/magnatune/eyecreate/companionformagnatune/login/ObscuredSharedPreferences.java @@ -0,0 +1,349 @@ +package com.magnatune.eyecreate.companionformagnatune.login; + +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 android.content.Context; +import android.content.SharedPreferences; +import android.provider.Settings; +import android.util.Base64; +import android.util.Log; + + +/** + * 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; + + //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 object + * To improve performance for multiple accesses, please store the returned object in a variable for reuse + * @param c - the context used to access the preferences. + * @param domainName - domain the shared preferences should be stored under + * @param contextMode - Typically Context.MODE_PRIVATE + * @return + */ + public synchronized static ObscuredSharedPreferences getPrefs(Context c, String domainName, int contextMode) { + //make sure to use application context since preferences live outside an Activity + //use for objects that have global scope like: prefs or starting services + ObscuredSharedPreferences prefs = new ObscuredSharedPreferences( + c.getApplicationContext(), c.getApplicationContext().getSharedPreferences(domainName, 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 values) { + throw new RuntimeException("This class does not work with String Sets."); + } + } + + public Editor edit() { + return new Editor(); + } + + + @Override + public Map 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 getStringSet(String key, Set 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(Base64.encode(pbeCipher.doFinal(bytes), Base64.NO_WRAP),UTF8); + } catch( Exception e ) { + throw new RuntimeException(e); + } + + } + + protected String decrypt(String value){ + try { + final byte[] bytes = value!=null ? Base64.decode(value,Base64.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; + } + } +} -- GitLab