package asia.fivejuly.securepreferences; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import com.facebook.android.crypto.keychain.AndroidConceal; import com.facebook.android.crypto.keychain.SharedPrefsBackedKeyChain; import com.facebook.crypto.Crypto; import com.facebook.crypto.CryptoConfig; import com.facebook.crypto.Entity; import com.facebook.crypto.exception.CryptoInitializationException; import com.facebook.crypto.exception.KeyChainException; import com.facebook.crypto.keychain.KeyChain; import com.facebook.soloader.SoLoader; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Created by haipq on 12/27/16. */ public class SecurePreferences implements SharedPreferences { private static final String TAG = SecurePreferences.class.getName(); private static boolean isDebug = false; private static boolean isInit = false; private SharedPreferences sharedPreferences; private Entity entity; private Crypto crypto; public final static class Builder { private Context context; private String password; private String sharedPrefFilename; public Builder(final Context context) { this.context = context; } public String password() { return password; } public Builder password(final String password) { this.password = password; return this; } public String filename() { return sharedPrefFilename; } public Builder filename(final String sharedPrefFilename) { this.sharedPrefFilename = sharedPrefFilename; return this; } public SharedPreferences build() { if(!isInit) { Log.w(TAG, "You need call 'SecurePreferences.init()' in onCreate() from your application class."); } KeyChain keyChain = new SharedPrefsBackedKeyChain(context, CryptoConfig.KEY_256); Entity entity = Entity.create( TextUtils.isEmpty(password) ? getClass().getPackage().getName() : password ); return new SecurePreferences( context, keyChain, entity, sharedPrefFilename ); } } private SecurePreferences(Context context, final KeyChain keyChain, final Entity entity, final String sharedPrefFilename) { this.entity = entity; this.sharedPreferences = getSharedPreferenceFile(context, sharedPrefFilename); this.crypto = AndroidConceal.get().createCrypto256Bits(keyChain); } public static void init(Context pContext) { SoLoader.init(pContext, false); isInit = true; } private String encrypt(final String plainText) { if (TextUtils.isEmpty(plainText)) { return plainText; } byte[] cipherText = null; if (!crypto.isAvailable()) { log(Log.WARN, "encrypt: crypto not available"); return null; } try { cipherText = crypto.encrypt(plainText.getBytes(), entity); } catch (KeyChainException | CryptoInitializationException | IOException e) { log(Log.ERROR, "encrypt: " + e); } return cipherText != null ? Base64.encodeToString(cipherText, Base64.DEFAULT) : null; } private String decrypt(final String encryptedText) { if (TextUtils.isEmpty(encryptedText)) { return encryptedText; } byte[] plainText = null; if (!crypto.isAvailable()) { log(Log.WARN, "decrypt: crypto not available"); return null; } try { plainText = crypto.decrypt(Base64.decode(encryptedText, Base64.DEFAULT), entity); } catch (KeyChainException | CryptoInitializationException | IOException e) { log(Log.ERROR, "decrypt: " + e); } return plainText != null ? new String(plainText) : null; } public static boolean isDebug() { return isDebug; } public static void setIsDebug(boolean isDebug) { SecurePreferences.isDebug = isDebug; } @Override public Map<String, ?> getAll() { final Map<String, ?> encryptedMap = sharedPreferences.getAll(); final Map<String, String> decryptedMap = new HashMap<>( encryptedMap.size()); for (Map.Entry<String, ?> entry : encryptedMap.entrySet()) { try { Object cipherText = entry.getValue(); if (cipherText != null) { decryptedMap.put(entry.getKey(), decrypt(cipherText.toString())); } } catch (Exception e) { log(Log.ERROR, "error getAll: " + e); decryptedMap.put(entry.getKey(), entry.getValue().toString()); } } return decryptedMap; } @Override public String getString(String key, String defaultValue) { final String encryptedValue = sharedPreferences.getString( SecurePreferences.hashKey(key), null); return (encryptedValue != null) ? decrypt(encryptedValue) : defaultValue; } @Override public Set<String> getStringSet(String key, Set<String> defaultValues) { final Set<String> encryptedSet = sharedPreferences.getStringSet( SecurePreferences.hashKey(key), null); if (encryptedSet == null) { return defaultValues; } final Set<String> decryptedSet = new HashSet<>( encryptedSet.size()); for (String encryptedValue : encryptedSet) { decryptedSet.add(decrypt(encryptedValue)); } return decryptedSet; } @Override public int getInt(String key, int defaultValue) { final String encryptedValue = sharedPreferences.getString( SecurePreferences.hashKey(key), null); if (encryptedValue == null) { return defaultValue; } try { return Integer.parseInt(decrypt(encryptedValue)); } catch (NumberFormatException e) { throw new ClassCastException(e.getMessage()); } } @Override public long getLong(String key, long defaultValue) { final String encryptedValue = sharedPreferences.getString( SecurePreferences.hashKey(key), null); if (encryptedValue == null) { return defaultValue; } try { return Long.parseLong(decrypt(encryptedValue)); } catch (NumberFormatException e) { throw new ClassCastException(e.getMessage()); } } @Override public float getFloat(String key, float defaultValue) { final String encryptedValue = sharedPreferences.getString( SecurePreferences.hashKey(key), null); if (encryptedValue == null) { return defaultValue; } try { return Float.parseFloat(decrypt(encryptedValue)); } catch (NumberFormatException e) { throw new ClassCastException(e.getMessage()); } } @Override public boolean getBoolean(String key, boolean defaultValue) { final String encryptedValue = sharedPreferences.getString( SecurePreferences.hashKey(key), null); if (encryptedValue == null) { return defaultValue; } try { return Boolean.parseBoolean(decrypt(encryptedValue)); } catch (NumberFormatException e) { throw new ClassCastException(e.getMessage()); } } @Override public boolean contains(String key) { return sharedPreferences.contains(SecurePreferences.hashKey(key)); } @Override public Editor edit() { return new Editor(); } public final class Editor implements SharedPreferences.Editor { private SharedPreferences.Editor mEditor; @SuppressLint("CommitPrefEdits") private Editor() { mEditor = sharedPreferences.edit(); } @Override public SharedPreferences.Editor putString(String key, String value) { mEditor.putString(SecurePreferences.hashKey(key), encrypt(value)); return this; } @Override public SharedPreferences.Editor putStringSet(String key, Set<String> values) { final Set<String> encryptedValues = new HashSet<>( values.size()); for (String value : values) { encryptedValues.add(encrypt(value)); } mEditor.putStringSet(SecurePreferences.hashKey(key), encryptedValues); return this; } @Override public SharedPreferences.Editor putInt(String key, int value) { mEditor.putString(SecurePreferences.hashKey(key), encrypt(Integer.toString(value))); return this; } @Override public SharedPreferences.Editor putLong(String key, long value) { mEditor.putString(SecurePreferences.hashKey(key), encrypt(Long.toString(value))); return this; } @Override public SharedPreferences.Editor putFloat(String key, float value) { mEditor.putString(SecurePreferences.hashKey(key), encrypt(Float.toString(value))); return this; } @Override public SharedPreferences.Editor putBoolean(String key, boolean value) { mEditor.putString(SecurePreferences.hashKey(key), encrypt(Boolean.toString(value))); return this; } @Override public SharedPreferences.Editor remove(String key) { mEditor.remove(SecurePreferences.hashKey(key)); return this; } @Override public SharedPreferences.Editor clear() { mEditor.clear(); return this; } @Override public boolean commit() { return mEditor.commit(); } @Override public void apply() { mEditor.apply(); } } @Override public void registerOnSharedPreferenceChangeListener( final OnSharedPreferenceChangeListener listener) { sharedPreferences .registerOnSharedPreferenceChangeListener(listener); } @Override public void unregisterOnSharedPreferenceChangeListener( final OnSharedPreferenceChangeListener listener) { sharedPreferences .unregisterOnSharedPreferenceChangeListener(listener); } private static String hashKey(String key) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] array = md.digest(key.getBytes()); StringBuffer sb = new StringBuffer(); for (byte anArray : array) { sb.append(Integer.toHexString((anArray & 0xFF) | 0x100).substring(1, 3)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { log(Log.WARN, " SecurePreferences.hashKey error: " + e); } return key; } private SharedPreferences getSharedPreferenceFile(Context context, String prefFilename) { if (TextUtils.isEmpty(prefFilename)) { return PreferenceManager .getDefaultSharedPreferences(context); } else { return context.getSharedPreferences(prefFilename, Context.MODE_PRIVATE); } } private static void log(int type, String str) { if (isDebug) { switch (type) { case Log.WARN: Log.w(TAG, str); break; case Log.ERROR: Log.e(TAG, str); break; case Log.DEBUG: Log.d(TAG, str); break; } } } }