Last active
August 29, 2015 14:01
-
-
Save snargledorf/2585619311fc29238030 to your computer and use it in GitHub Desktop.
SecurePreferences
This file contains 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
package com.theeste.securepreferences; | |
/* | |
* Copyright (C) 2013, Daniel Abraham | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
import android.annotation.TargetApi; | |
import android.content.Context; | |
import android.content.SharedPreferences; | |
import android.os.Build; | |
import android.preference.PreferenceManager; | |
import android.provider.Settings; | |
import android.util.Base64; | |
import android.util.Log; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.SecureRandom; | |
import java.security.spec.InvalidKeySpecException; | |
import java.security.spec.KeySpec; | |
import java.util.HashMap; | |
import java.util.HashSet; | |
import java.util.Map; | |
import java.util.Map.Entry; | |
import java.util.Set; | |
import javax.crypto.Cipher; | |
import javax.crypto.KeyGenerator; | |
import javax.crypto.SecretKeyFactory; | |
import javax.crypto.spec.PBEKeySpec; | |
import javax.crypto.spec.SecretKeySpec; | |
/** | |
* Wrapper class for Android's {@link SharedPreferences} interface, which adds a layer of | |
* encryption to the persistent storage and retrieval of sensitive key-value pairs of primitive | |
* data types. | |
* <p> | |
* This class provides important - but nevertheless imperfect - protection against simple attacks | |
* by casual snoopers. It is crucial to remember that even encrypted data may still be susceptible | |
* to attacks, especially on rooted or stolen devices! | |
* <p> | |
* This class requires API level 8 (Android 2.2, a.k.a. "Froyo") or greater. | |
* | |
* @see <a href="http://www.codeproject.com/Articles/549119/Encryption-Wrapper-for-Android-SharedPreferences">CodeProject article</a> | |
*/ | |
public class SecurePreferences implements SharedPreferences { | |
private static SharedPreferences sFile; | |
private static byte[] sKey; | |
/** | |
* Constructor. | |
* | |
* @param context the caller's context | |
*/ | |
public SecurePreferences(Context context) { | |
// Proxy design pattern | |
if (SecurePreferences.sFile == null) { | |
SecurePreferences.sFile = PreferenceManager.getDefaultSharedPreferences(context); | |
} | |
// Initialize encryption/decryption key | |
try { | |
final String key = SecurePreferences.generateAesKeyName(context); | |
String value = SecurePreferences.sFile.getString(key, null); | |
if (value == null) { | |
value = SecurePreferences.generateAesKeyValue(); | |
SecurePreferences.sFile.edit().putString(key, value).commit(); | |
} | |
SecurePreferences.sKey = SecurePreferences.decode(value); | |
} catch (Exception e) { | |
throw new IllegalStateException(e); | |
} | |
} | |
private static String encode(byte[] input) | |
{ | |
return Base64.encodeToString(input, Base64.NO_PADDING | Base64.NO_WRAP); | |
} | |
private static byte[] decode(String input) | |
{ | |
return Base64.decode(input, Base64.NO_PADDING | Base64.NO_WRAP); | |
} | |
private static String generateAesKeyName(Context context) throws InvalidKeySpecException, | |
NoSuchAlgorithmException { | |
final char[] password = context.getPackageName().toCharArray(); | |
final byte[] salt = Settings.Secure.getString(context.getContentResolver(), | |
Settings.Secure.ANDROID_ID).getBytes(); | |
// Number of PBKDF2 hardening rounds to use, larger values increase | |
// computation time, you should select a value that causes | |
// computation to take >100ms | |
final int iterations = 1000; | |
// Generate a 256-bit key | |
final int keyLength = 256; | |
final KeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength); | |
return SecurePreferences.encode(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") | |
.generateSecret(spec).getEncoded()); | |
} | |
private static String generateAesKeyValue() throws NoSuchAlgorithmException { | |
// Do *not* seed secureRandom! Automatically seeded from system entropy | |
final SecureRandom random = new SecureRandom(); | |
// Use the largest AES key length which is supported by the OS | |
final KeyGenerator generator = KeyGenerator.getInstance("AES"); | |
try { | |
generator.init(256, random); | |
} catch (Exception e) { | |
try { | |
generator.init(192, random); | |
} catch (Exception e1) { | |
generator.init(128, random); | |
} | |
} | |
return SecurePreferences.encode(generator.generateKey().getEncoded()); | |
} | |
private static String encrypt(String cleartext) { | |
if (cleartext == null || cleartext.length() == 0) { | |
return cleartext; | |
} | |
try { | |
final Cipher cipher = Cipher.getInstance("AES"); | |
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(SecurePreferences.sKey, "AES")); | |
return SecurePreferences.encode(cipher.doFinal(cleartext.getBytes("UTF-8"))); | |
} catch (Exception e) { | |
Log.w(SecurePreferences.class.getName(), "encrypt", e); | |
return null; | |
} | |
} | |
private static String decrypt(String ciphertext) { | |
if (ciphertext == null || ciphertext.length() == 0) { | |
return ciphertext; | |
} | |
try { | |
final Cipher cipher = Cipher.getInstance("AES"); | |
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(SecurePreferences.sKey, "AES")); | |
return new String(cipher.doFinal(SecurePreferences.decode(ciphertext)), "UTF-8"); | |
} catch (Exception e) { | |
Log.w(SecurePreferences.class.getName(), "decrypt", e); | |
return null; | |
} | |
} | |
@Override | |
public Map<String, String> getAll() { | |
final Map<String, ?> encryptedMap = SecurePreferences.sFile.getAll(); | |
final Map<String, String> decryptedMap = new HashMap<String, String>(encryptedMap.size()); | |
for (Entry<String, ?> entry : encryptedMap.entrySet()) { | |
try { | |
decryptedMap.put(SecurePreferences.decrypt(entry.getKey()), | |
SecurePreferences.decrypt(entry.getValue().toString())); | |
} catch (Exception e) { | |
decryptedMap.put(entry.getKey(), entry.getValue().toString()); | |
} | |
} | |
return decryptedMap; | |
} | |
@Override | |
public String getString(String key, String defaultValue) { | |
// Check if an unencrypted entry exists and encrypt it | |
if (SecurePreferences.sFile.contains(key)) { | |
String value = SecurePreferences.sFile.getString(key, defaultValue); | |
SecurePreferences.sFile.edit().remove(key).apply(); | |
this.edit().putString(key, value).apply(); | |
return value; | |
} | |
final String encryptedValue = | |
SecurePreferences.sFile.getString(SecurePreferences.encrypt(key), null); | |
return (encryptedValue != null) ? SecurePreferences.decrypt(encryptedValue) : defaultValue; | |
} | |
@Override | |
@TargetApi(Build.VERSION_CODES.HONEYCOMB) | |
public Set<String> getStringSet(String key, Set<String> defaultValues) { | |
// Check if an unencrypted entry exists and encrypt it | |
if (SecurePreferences.sFile.contains(key)) { | |
Set<String> value = SecurePreferences.sFile.getStringSet(key, defaultValues); | |
SecurePreferences.sFile.edit().remove(key).apply(); | |
this.edit().putStringSet(key, value).apply(); | |
return value; | |
} | |
final Set<String> encryptedSet = | |
SecurePreferences.sFile.getStringSet(SecurePreferences.encrypt(key), null); | |
if (encryptedSet == null) { | |
return defaultValues; | |
} | |
final Set<String> decryptedSet = new HashSet<String>(encryptedSet.size()); | |
for (String encryptedValue : encryptedSet) { | |
decryptedSet.add(SecurePreferences.decrypt(encryptedValue)); | |
} | |
return decryptedSet; | |
} | |
@Override | |
public int getInt(String key, int defaultValue) { | |
// Check if an unencrypted entry exists and encrypt it | |
if (SecurePreferences.sFile.contains(key)) { | |
int value = SecurePreferences.sFile.getInt(key, defaultValue); | |
SecurePreferences.sFile.edit().remove(key).apply(); | |
this.edit().putInt(key, value).apply(); | |
return value; | |
} | |
final String value = getString(key, null); | |
if (value == null) { | |
return defaultValue; | |
} | |
try { | |
return Integer.parseInt(value); | |
} catch (NumberFormatException e) { | |
throw new ClassCastException(e.getMessage()); | |
} | |
} | |
@Override | |
public long getLong(String key, long defaultValue) { | |
// Check if an unencrypted entry exists and encrypt it | |
if (SecurePreferences.sFile.contains(key)) { | |
long value = SecurePreferences.sFile.getLong(key, defaultValue); | |
SecurePreferences.sFile.edit().remove(key).apply(); | |
this.edit().putLong(key, value).apply(); | |
return value; | |
} | |
final String value = getString(key, null); | |
if (value == null) { | |
return defaultValue; | |
} | |
try { | |
return Long.parseLong(value); | |
} catch (NumberFormatException e) { | |
throw new ClassCastException(e.getMessage()); | |
} | |
} | |
@Override | |
public float getFloat(String key, float defaultValue) { | |
// Check if an unencrypted entry exists and encrypt it | |
if (SecurePreferences.sFile.contains(key)) { | |
float value = SecurePreferences.sFile.getFloat(key, defaultValue); | |
SecurePreferences.sFile.edit().remove(key).apply(); | |
this.edit().putFloat(key, value).apply(); | |
return value; | |
} | |
final String value = getString(key, null); | |
if (value == null) { | |
return defaultValue; | |
} | |
try { | |
return Float.parseFloat(value); | |
} catch (NumberFormatException e) { | |
throw new ClassCastException(e.getMessage()); | |
} | |
} | |
@Override | |
public boolean getBoolean(String key, boolean defaultValue) { | |
// Check if an unencrypted entry exists and encrypt it | |
if (SecurePreferences.sFile.contains(key)) { | |
boolean value = SecurePreferences.sFile.getBoolean(key, defaultValue); | |
SecurePreferences.sFile.edit().remove(key).apply(); | |
this.edit().putBoolean(key, value).apply(); | |
return value; | |
} | |
final String value = getString(key, null); | |
if (value == null) { | |
return defaultValue; | |
} | |
try { | |
return Boolean.parseBoolean(value); | |
} catch (NumberFormatException e) { | |
throw new ClassCastException(e.getMessage()); | |
} | |
} | |
@Override | |
public boolean contains(String key) { | |
return SecurePreferences.sFile.contains(key) || SecurePreferences.sFile.contains(SecurePreferences.encrypt(key)); | |
} | |
@Override | |
public Editor edit() { | |
return new Editor(); | |
} | |
/** | |
* Wrapper for Android's {@link android.content.SharedPreferences.Editor}. | |
* <p> | |
* Used for modifying values in a {@link SecurePreferences} object. All changes you make in an | |
* editor are batched, and not copied back to the original {@link SecurePreferences} until you | |
* call {@link #commit()} or {@link #apply()}. | |
*/ | |
public static class Editor implements SharedPreferences.Editor { | |
private SharedPreferences.Editor mEditor; | |
/** | |
* Constructor. | |
*/ | |
private Editor() { | |
mEditor = SecurePreferences.sFile.edit(); | |
} | |
@Override | |
public SharedPreferences.Editor putString(String key, String value) { | |
SecurePreferences.sFile.edit().remove(key).apply(); | |
mEditor.putString(SecurePreferences.encrypt(key), SecurePreferences.encrypt(value)); | |
return this; | |
} | |
@Override | |
@TargetApi(Build.VERSION_CODES.HONEYCOMB) | |
public SharedPreferences.Editor putStringSet(String key, Set<String> values) { | |
SecurePreferences.sFile.edit().remove(key).apply(); | |
final Set<String> encryptedValues = new HashSet<String>(values.size()); | |
for (String value : values) { | |
encryptedValues.add(SecurePreferences.encrypt(value)); | |
} | |
mEditor.putStringSet(SecurePreferences.encrypt(key), encryptedValues); | |
return this; | |
} | |
@Override | |
public SharedPreferences.Editor putInt(String key, int value) { | |
putString(key, Integer.toString(value)); | |
return this; | |
} | |
@Override | |
public SharedPreferences.Editor putLong(String key, long value) { | |
putString(key, Long.toString(value)); | |
return this; | |
} | |
@Override | |
public SharedPreferences.Editor putFloat(String key, float value) { | |
putString(key, Float.toString(value)); | |
return this; | |
} | |
@Override | |
public SharedPreferences.Editor putBoolean(String key, boolean value) { | |
putString(key, Boolean.toString(value)); | |
return this; | |
} | |
@Override | |
public SharedPreferences.Editor remove(String key) { | |
SecurePreferences.sFile.edit().remove(key).apply(); | |
mEditor.remove(SecurePreferences.encrypt(key)); | |
return this; | |
} | |
@Override | |
public SharedPreferences.Editor clear() { | |
mEditor.clear(); | |
return this; | |
} | |
@Override | |
public boolean commit() { | |
return mEditor.commit(); | |
} | |
@Override | |
@TargetApi(Build.VERSION_CODES.GINGERBREAD) | |
public void apply() { | |
mEditor.apply(); | |
} | |
} | |
@Override | |
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { | |
SecurePreferences.sFile.registerOnSharedPreferenceChangeListener(listener); | |
} | |
@Override | |
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { | |
SecurePreferences.sFile.unregisterOnSharedPreferenceChangeListener(listener); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment