/*
* 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.
*/
package com.aptoide.amethyst.preferences;
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.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import com.aptoide.amethyst.Aptoide;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
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.SecretKey;
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>
*
* @see <a
* href="http://www.codeproject.com/Articles/549119/Encryption-Wrapper-for-Android-SharedPreferences">CodeProject
* article</a>
*/
public class SecurePreferences implements SharedPreferences {
private static final int KEY_SIZE = 256;
// requires Spongycastle crypto libraries
// private static final String AES_KEY_ALG = "AES/GCM/NoPadding";
// private static final String AES_KEY_ALG = "AES/CBC/PKCS5Padding";
private static final String AES_KEY_ALG = "AES";
private static final String PRIMARY_PBE_KEY_ALG = "PBKDF2WithHmacSHA1";
private static final String BACKUP_PBE_KEY_ALG = "PBEWithMD5AndDES";
private static final int ITERATIONS = 2000;
// change to SC if using Spongycastle crypto libraries
private static final String PROVIDER = "BC";
private static SharedPreferences sFile;
private static byte[] sKey;
private static boolean sLoggingEnabled = false;
private static final String TAG = SecurePreferences.class.getName();
/**
* Constructor.
*
* @param context
* the caller's context
*/
private static SharedPreferences singleton = null;
/* private static class SingletonHolder {
private static final SharedPreferences INSTANCE= new SecurePreferences(Aptoide.getContext());
}
public static SharedPreferences getInstance() {
return SingletonHolder.INSTANCE;
}*/
public static synchronized SharedPreferences getInstance(){
if(singleton==null) {
if (Build.VERSION.SDK_INT < 8) {
singleton = PreferenceManager.getDefaultSharedPreferences(Aptoide.getContext());
} else {
singleton = new SecurePreferences(Aptoide.getContext());
}
}
return singleton;
}
private 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) {
if (sLoggingEnabled) {
Log.e(TAG, "Error init:" + e.getMessage());
}
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,
NoSuchProviderException {
final char[] password = context.getPackageName().toCharArray();
final byte[] salt = getDeviceSerialNumber(context).getBytes();
SecretKey key;
try {
// what if there's an OS upgrade and now supports the primary
// PBE
key = SecurePreferences.generatePBEKey(password, salt,
PRIMARY_PBE_KEY_ALG, ITERATIONS, KEY_SIZE);
} catch (NoSuchAlgorithmException e) {
// older devices may not support the have the implementation,
// try with a weaker algorithm
key = SecurePreferences.generatePBEKey(password, salt,
BACKUP_PBE_KEY_ALG, ITERATIONS, KEY_SIZE);
}
return SecurePreferences.encode(key.getEncoded());
}
/**
* Derive a secure key based on the passphraseOrPin
*
* @param passphraseOrPin
* @param salt
* @param algorthm
* - which PBE algorthm to use. some <4.0 devices don;t support
* the prefered PBKDF2WithHmacSHA1
* @param iterations
* - Number of PBKDF2 hardening rounds to use. Larger values
* increase computation time (a good thing), defaults to 1000 if
* not set.
* @param keyLength
* @return Derived Secretkey
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @throws NoSuchProviderException
*/
private static SecretKey generatePBEKey(char[] passphraseOrPin,
byte[] salt, String algorthm, int iterations, int keyLength)
throws NoSuchAlgorithmException, InvalidKeySpecException,
NoSuchProviderException {
if (iterations == 0) {
iterations = 1000;
}
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(
algorthm, PROVIDER);
KeySpec keySpec = new PBEKeySpec(passphraseOrPin, salt, iterations,
keyLength);
SecretKey secretKey = secretKeyFactory.generateSecret(keySpec);
return secretKey;
}
/**
* Gets the hardware serial number of this device.
*
* @return serial number or Settings.Secure.ANDROID_ID if not available.
*/
private static String getDeviceSerialNumber(Context context) {
// We're using the Reflection API because Build.SERIAL is only available
// since API Level 9 (Gingerbread, Android 2.3).
try {
String deviceSerial = (String) Build.class.getField("SERIAL").get(
null);
if (TextUtils.isEmpty(deviceSerial)) {
deviceSerial = Settings.Secure.getString(
context.getContentResolver(),
Settings.Secure.ANDROID_ID);
}
return deviceSerial;
} catch (Exception ignored) {
// default to Android_ID
return Settings.Secure.getString(context.getContentResolver(),
Settings.Secure.ANDROID_ID);
}
}
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(KEY_SIZE, 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_KEY_ALG, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(
SecurePreferences.sKey, AES_KEY_ALG));
return SecurePreferences.encode(cipher.doFinal(cleartext
.getBytes("UTF-8")));
} catch (Exception e) {
if (sLoggingEnabled) {
Log.w(TAG, "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_KEY_ALG, PROVIDER);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(
SecurePreferences.sKey, AES_KEY_ALG));
return new String(cipher.doFinal(SecurePreferences
.decode(ciphertext)), "UTF-8");
} catch (Exception e) {
if (sLoggingEnabled) {
Log.w(TAG, "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) {
// Ignore unencrypted key/value pairs
}
}
return decryptedMap;
}
@Override
public String getString(String key, String defaultValue) {
final String encryptedValue = SecurePreferences.sFile.getString(
SecurePreferences.encrypt(key), null);
return (encryptedValue != null) ? SecurePreferences
.decrypt(encryptedValue) : defaultValue;
}
/**
*
* Added to get a values as as it can be useful to store values that are
* already encrypted and encoded
*
* @param key
* @param defaultValue
* @return Unencrypted value of the key or the defaultValue if
*/
public String getStringUnencrypted(String key, String defaultValue) {
final String nonEncryptedValue = SecurePreferences.sFile.getString(
SecurePreferences.encrypt(key), null);
return (nonEncryptedValue != null) ? nonEncryptedValue : defaultValue;
}
@Override
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public Set<String> getStringSet(String key, Set<String> defaultValues) {
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) {
final String encryptedValue = SecurePreferences.sFile.getString(
SecurePreferences.encrypt(key), null);
if (encryptedValue == null) {
return defaultValue;
}
try {
return Integer.parseInt(SecurePreferences.decrypt(encryptedValue));
} catch (NumberFormatException e) {
throw new ClassCastException(e.getMessage());
}
}
@Override
public long getLong(String key, long defaultValue) {
final String encryptedValue = SecurePreferences.sFile.getString(
SecurePreferences.encrypt(key), null);
if (encryptedValue == null) {
return defaultValue;
}
try {
return Long.parseLong(SecurePreferences.decrypt(encryptedValue));
} catch (NumberFormatException e) {
throw new ClassCastException(e.getMessage());
}
}
@Override
public float getFloat(String key, float defaultValue) {
final String encryptedValue = SecurePreferences.sFile.getString(
SecurePreferences.encrypt(key), null);
if (encryptedValue == null) {
return defaultValue;
}
try {
return Float.parseFloat(SecurePreferences.decrypt(encryptedValue));
} catch (NumberFormatException e) {
throw new ClassCastException(e.getMessage());
}
}
@Override
public boolean getBoolean(String key, boolean defaultValue) {
final String encryptedValue = SecurePreferences.sFile.getString(
SecurePreferences.encrypt(key), null);
if (encryptedValue == null) {
return defaultValue;
}
try {
return Boolean.parseBoolean(SecurePreferences
.decrypt(encryptedValue));
} catch (NumberFormatException e) {
throw new ClassCastException(e.getMessage());
}
}
@Override
public boolean contains(String key) {
return SecurePreferences.sFile.contains(SecurePreferences.encrypt(key));
}
@Override
public Editor edit() {
return new Editor();
}
/**
* Wrapper for Android's {@link 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) {
mEditor.putString(SecurePreferences.encrypt(key),
SecurePreferences.encrypt(value));
return this;
}
/**
* This is useful for storing values that have be encrypted by something
* else
*
* @param key
* - encrypted as usual
* @param value
* will not be encrypted
* @return
*/
public SharedPreferences.Editor putStringNoEncrypted(String key,
String value) {
mEditor.putString(SecurePreferences.encrypt(key), value);
return this;
}
@Override
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public SharedPreferences.Editor putStringSet(String key,
Set<String> values) {
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) {
mEditor.putString(SecurePreferences.encrypt(key),
SecurePreferences.encrypt(Integer.toString(value)));
return this;
}
@Override
public SharedPreferences.Editor putLong(String key, long value) {
mEditor.putString(SecurePreferences.encrypt(key),
SecurePreferences.encrypt(Long.toString(value)));
return this;
}
@Override
public SharedPreferences.Editor putFloat(String key, float value) {
mEditor.putString(SecurePreferences.encrypt(key),
SecurePreferences.encrypt(Float.toString(value)));
return this;
}
@Override
public SharedPreferences.Editor putBoolean(String key, boolean value) {
mEditor.putString(SecurePreferences.encrypt(key),
SecurePreferences.encrypt(Boolean.toString(value)));
return this;
}
@Override
public SharedPreferences.Editor remove(String key) {
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() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
mEditor.apply();
} else {
commit();
}
}
}
public static boolean isLoggingEnabled() {
return sLoggingEnabled;
}
public static void setLoggingEnabled(boolean loggingEnabled) {
sLoggingEnabled = loggingEnabled;
}
@Override
public void registerOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) {
SecurePreferences.sFile
.registerOnSharedPreferenceChangeListener(listener);
}
@Override
public void unregisterOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) {
SecurePreferences.sFile
.unregisterOnSharedPreferenceChangeListener(listener);
}
}