Skip to content

Instantly share code, notes, and snippets.

@snargledorf
Last active August 29, 2015 14:01
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 snargledorf/2585619311fc29238030 to your computer and use it in GitHub Desktop.
Save snargledorf/2585619311fc29238030 to your computer and use it in GitHub Desktop.
SecurePreferences
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