package oak.app; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; 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.Set; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.PBEParameterSpec; import javax.crypto.spec.SecretKeySpec; import oak.util.Base64; /** * Created by robcook on 4/2/14. * <p/> * Warning, this gives a false sense of security. If an attacker has enough access to acquire your * password store, then he almost certainly has enough access to acquire your source binary and * figure out your encryption key. However, it will prevent casual investigators from acquiring * passwords, and thereby may prevent undesired negative publicity. */ /** * Warning, this gives a false sense of security. If an attacker has enough access to acquire your * password store, then he almost certainly has enough access to acquire your source binary and * figure out your encryption key. However, it will prevent casual investigators from acquiring * passwords, and thereby may prevent undesired negative publicity. * */ /** * This code originally posted by Michael Burton on StackOverflow * http://stackoverflow.com/questions/785973/what-is-the-most-appropriate-way-to-store-user-settings-in-android-application/6393502#6393502 * <p/> * This class was created to replace the original ObscurredSharedPreferences. It includes DES and AES * encryption options, and uses a randomly generate initialization vector to ensure each encrypted string * is unique. If no crypto type is specified the default is AES. * <p/> * Note: There was a flaw in the JCA in some versions of Android. There is a fix, * see this: http://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html * <p/> * The code provided is also provided in OAK. You should call PRNGFixes.apply() * in your Application.onCreate() method if you are using this library to ensure * strong keys are created. */ public abstract class CryptoSharedPreferences implements SharedPreferences { private static final String CRYPTO_TYPE_KEY = "CryptoSharedPrefs_Type_Key"; protected static final String UTF8 = "utf-8"; protected static final int SECRET_KEY_ITERATIONS = 100; protected static final int IV_LENGTH = 16; protected static final String RANDOM_ALGORITHM = "SHA1PRNG"; // AES protected static final String CIPHER_ALGORITHM_AES = "AES/CBC/PKCS5Padding"; protected static final String PBE_ALGORITHM_AES = "PBKDF2WithHmacSHA1"; private static final String SECRET_KEY_ALGORITHM_AES = "AES"; // DES protected static final String CIPHER_ALGORITHM_DES = "PBEWithMD5AndDES"; protected static final String PBE_ALGORITHM_DES = "PBEWithMD5AndDES"; protected SharedPreferences delegate; protected Context context; private int cryptoToUse; public static final int CRYPTO_AES = 0x0000; public static final int CRYPTO_DES = 0x0001; /** * A salt is required to ensure the AES key is the correct length. It * is not necessary to have a random salt because we're using an initialization * vector to ensure the encrypted data is effectively random each time it is * generated. */ private static byte[] SALT = new byte[]{(byte) 0x162, 0x48, (byte) 0x1c9, (byte) 0x2d8, (byte) 0x283, (byte) 0xc8, (byte) 0xeb, (byte) 0x148, (byte) 0x1bb, (byte) 0x2c7, (byte) 0x246, (byte) 0x114, (byte) 0x2e0, (byte) 0x140, (byte) 0x1b8, (byte) 0x114, (byte) 0xbd, (byte) 0x321, (byte) 0x1d7, (byte) 0x1dd}; public CryptoSharedPreferences(Context context, SharedPreferences delegate) { this.delegate = delegate; this.context = context; initCryptoType(); } /** * AES is preferred but there is some indication AES is not available on * all phones (http://www.unwesen.de/2011/06/12/encryption-on-android-bouncycastle/). * To mitigate this the phone is checked for supporting the AES encryption * standard and either AES or DES is saved in shared preferences. This * ensures the same type of encryption is used even if the phone gains * AES support from an upgrade. */ private void initCryptoType() { int cryptoType = delegate.getInt(CRYPTO_TYPE_KEY, -1); if (cryptoType == CRYPTO_AES || cryptoType == CRYPTO_DES) { this.cryptoToUse = cryptoType; return; } this.cryptoToUse = CRYPTO_AES; try { Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM_AES); } catch (NoSuchAlgorithmException ae) { ae.printStackTrace(); this.cryptoToUse = CRYPTO_DES; } catch (NoSuchPaddingException pe) { pe.printStackTrace(); this.cryptoToUse = CRYPTO_DES; } delegate.edit().putInt(CRYPTO_TYPE_KEY, this.cryptoToUse).commit(); } /** * Implement this method to supply your char array with your password. * Ideally this is from user input or an external api and not a hard-coded * string. * * @return */ protected abstract char[] getSpecialCode(); public class Editor implements SharedPreferences.Editor { protected SharedPreferences.Editor delegate; public Editor() { this.delegate = CryptoSharedPreferences.this.delegate.edit(); } @Override public Editor putBoolean(String key, boolean value) { String eValue = encrypt(Boolean.toString(value)); delegate.putString(key, eValue); 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; } public Editor putStringSet(String s, Set<String> strings) { Set<String> encrypted = new HashSet<String>(strings.size()); for (String stringToEncrypt : strings) { encrypted.add(CryptoSharedPreferences.this.encrypt(stringToEncrypt)); } this.delegate.putStringSet(s, encrypted); return this; } @Override public Editor clear() { delegate.clear(); return this; } @Override public boolean commit() { return delegate.commit(); } @Override public void apply() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { delegate.apply(); } } @Override public Editor remove(String s) { delegate.remove(s); return this; } } public Editor edit() { return new Editor(); } @Override public Map<String, ?> getAll() { final Map<String, ?> encryptedMap = this.delegate.getAll(); final Map<String, String> decryptedMap = new HashMap<String, String>( 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) { decryptedMap.put(entry.getKey(), entry.getValue().toString()); } } return decryptedMap; } @Override public boolean getBoolean(String key, boolean defValue) { final String v = delegate.getString(key, null); return v != null ? Boolean.parseBoolean(decrypt(v)) : defValue; } @Override public float getFloat(String key, float defValue) { final String v = delegate.getString(key, null); return v != null ? Float.parseFloat(decrypt(v)) : defValue; } @Override public int getInt(String key, int defValue) { final String v = delegate.getString(key, null); return v != null ? Integer.parseInt(decrypt(v)) : defValue; } @Override public long getLong(String key, long defValue) { final String v = delegate.getString(key, null); return v != null ? Long.parseLong(decrypt(v)) : defValue; } @Override public String getString(String key, String defValue) { final String v = delegate.getString(key, null); return v != null ? decrypt(v) : defValue; } @Override public Set<String> getStringSet(String s, Set<String> strings) { Set<String> savedStrings = this.delegate.getStringSet(s, strings); if (savedStrings == null) { return strings; } Set<String> decrypted = new HashSet<String>(strings.size()); for (String v : savedStrings) { decrypted.add(this.decrypt(v)); } return decrypted; } @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); } protected String encrypt(String value) { if (CRYPTO_DES == cryptoToUse) { return encrypt_DES(value); } return encrypt_AES(value); } protected String decrypt(String value) { if (CRYPTO_DES == cryptoToUse) { return decrypt_DES(value); } return decrypt_AES(value); } protected String encrypt_AES(String value) { try { final byte[] bytes = value != null ? value.getBytes(UTF8) : new byte[0]; byte[] ivBytes = getInitVector(); final IvParameterSpec iv = new IvParameterSpec(ivBytes); final SecretKey symKey = getSecretKey_AES(SALT); Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM_AES); cipher.init(Cipher.ENCRYPT_MODE, symKey, iv); byte[] encryptedBytes = cipher.doFinal(bytes); byte[] encryptedAndIv = new byte[encryptedBytes.length + ivBytes.length]; System.arraycopy(encryptedBytes, 0, encryptedAndIv, 0, encryptedBytes.length); System.arraycopy(ivBytes, 0, encryptedAndIv, encryptedBytes.length, ivBytes.length); return new String(Base64.encode(encryptedAndIv, Base64.NO_WRAP), UTF8); } catch (Exception e) { throw new RuntimeException(e); } } protected String decrypt_AES(String value) { try { final byte[] bytes = value != null ? Base64.decode(value, Base64.DEFAULT) : new byte[0]; byte[] ivBytes; byte[] encryptedBytes; if (bytes.length > IV_LENGTH) { ivBytes = copyOfRange(bytes, bytes.length - IV_LENGTH, bytes.length); encryptedBytes = copyOfRange(bytes, 0, bytes.length - IV_LENGTH); } else { return ""; } final IvParameterSpec iv = new IvParameterSpec(ivBytes); final SecretKey symKey = getSecretKey_AES(SALT); Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM_AES); cipher.init(Cipher.DECRYPT_MODE, symKey, iv); return new String(cipher.doFinal(encryptedBytes), UTF8); } catch (Exception e) { throw new RuntimeException(e); } } protected String encrypt_DES(String value) { try { final byte[] bytes = value != null ? value.getBytes(UTF8) : new byte[0]; byte[] ivBytes = getInitVector(); PBEParameterSpec iv = new PBEParameterSpec(ivBytes, 20); SecretKey key = getSecretKey_DES(); Cipher pbeCipher = Cipher.getInstance(CIPHER_ALGORITHM_DES); pbeCipher.init(Cipher.ENCRYPT_MODE, key, iv); byte[] encryptedBytes = pbeCipher.doFinal(bytes); byte[] encryptedAndIv = new byte[encryptedBytes.length + ivBytes.length]; System.arraycopy(encryptedBytes, 0, encryptedAndIv, 0, encryptedBytes.length); System.arraycopy(ivBytes, 0, encryptedAndIv, encryptedBytes.length, ivBytes.length); return new String(Base64.encode(encryptedAndIv, Base64.NO_WRAP), UTF8); } catch (Exception e) { throw new RuntimeException(e); } } protected String decrypt_DES(String value) { try { final byte[] bytes = value != null ? Base64.decode(value, Base64.DEFAULT) : new byte[0]; byte[] ivBytes; byte[] encryptedBytes; if (bytes.length > IV_LENGTH) { ivBytes = copyOfRange(bytes, bytes.length - IV_LENGTH, bytes.length); encryptedBytes = copyOfRange(bytes, 0, bytes.length - IV_LENGTH); } else { return ""; } PBEParameterSpec iv = new PBEParameterSpec(ivBytes, 20); SecretKey key = getSecretKey_DES(); Cipher pbeCipher = Cipher.getInstance(CIPHER_ALGORITHM_DES); pbeCipher.init(Cipher.DECRYPT_MODE, key, iv); return new String(pbeCipher.doFinal(encryptedBytes), UTF8); } catch (Exception e) { throw new RuntimeException(e); } } private byte[] getInitVector() throws NoSuchAlgorithmException { SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM); byte[] iv = new byte[IV_LENGTH]; random.nextBytes(iv); return iv; } private SecretKey getSecretKey_DES() throws NoSuchAlgorithmException, InvalidKeySpecException { PBEKeySpec keySpec = new PBEKeySpec(getSpecialCode()); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(PBE_ALGORITHM_DES); SecretKey key = keyFactory.generateSecret(keySpec); return key; } private SecretKey getSecretKey_AES(byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { SecretKeyFactory factory = SecretKeyFactory.getInstance(PBE_ALGORITHM_AES); KeySpec spec = new PBEKeySpec(getSpecialCode(), salt, SECRET_KEY_ITERATIONS, 256); SecretKey tmp = factory.generateSecret(spec); SecretKey secret = new SecretKeySpec(tmp.getEncoded(), SECRET_KEY_ALGORITHM_AES); return secret; } private byte[] copyOfRange(byte[] from, int start, int end) { int length = end - start; byte[] result = new byte[length]; System.arraycopy(from, start, result, 0, length); return result; } }