A partir de un post de StackOverflow (si alguien lo encuentra, pasadme el enlace y lo incluiré), encontré una implementación con cierto nivel de encriptación al que le hice una serie de modificaciones para hacerlo algo más seguro. Hay una segunda versión con medidas como "Reflexión" y cosas más avanzadas, pero que hace que el código no sea legible, por lo que la publicaré en otra entrada posterior.
Se utiliza así:
final SharedPreferences prefs = new ObscuredSharedPreferences( this, this.getSharedPreferences(MY_PREFS_FILE_NAME, Context.MODE_PRIVATE) ); // Test prefs.edit().putString("password","123456").commit(); Log.d("Test", "Password:" + prefs.getString("password", null));Y el código de la clase modificada sigue a continuación. La primera modificación que añadí al código original fue poner la contraseña en un recurso (R.string.obscure_prefs_password) porque así sólo con ApkTool no haces nada. Y la segunda es encriptar también los keys, para no dar pistas de que cosas se guardan en local. Es código para uso personal, por lo que no está preparado para subirlo a un repo y compartirlo (aunque para probarlo con crear el recurso de la clave y modificar el import de los R debe funcionar.
package com.itiox.content; import java.util.Map; import java.util.Set; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.PBEParameterSpec; import com.itiox.R; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.provider.Settings; import android.util.Base64; /** * 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. */ public class ObscuredSharedPreferences implements SharedPreferences { protected static final String UTF8 = "utf-8"; private final char[] SEKRIT; // INSERT A RANDOM PASSWORD HERE. // Don't use anything you wouldn't want to // get out there if someone decompiled // your app. protected SharedPreferences delegate; protected Context context; public ObscuredSharedPreferences(Context context, SharedPreferences delegate) { this.delegate = delegate; this.context = context; SEKRIT = context.getResources().getString(R.string.obscure_prefs_password).toCharArray(); } public class Editor implements SharedPreferences.Editor { protected SharedPreferences.Editor delegate; public Editor() { this.delegate = ObscuredSharedPreferences.this.delegate.edit(); } @Override public Editor putBoolean(String key, boolean value) { delegate.putString(encrypt(key), encrypt(Boolean.toString(value))); return this; } @Override public Editor putFloat(String key, float value) { delegate.putString(encrypt(key), encrypt(Float.toString(value))); return this; } @Override public Editor putInt(String key, int value) { delegate.putString(encrypt(key), encrypt(Integer.toString(value))); return this; } @Override public Editor putLong(String key, long value) { delegate.putString(encrypt(key), encrypt(Long.toString(value))); return this; } @Override public Editor putString(String key, String value) { delegate.putString(encrypt(key), encrypt(value)); return this; } @SuppressLint("NewApi") @Override public void apply() { delegate.apply(); } @Override public Editor clear() { delegate.clear(); return this; } @Override public boolean commit() { return delegate.commit(); } @Override public Editor remove(String s) { delegate.remove(s); return this; } @Override public android.content.SharedPreferences.Editor putStringSet( String arg0, Setarg1) { // TODO Auto-generated method stub return null; } } public Editor edit() { return new Editor(); } @Override public Map getAll() { throw new UnsupportedOperationException(); // left as an exercise to the reader } @Override public boolean getBoolean(String key, boolean defValue) { final String v = delegate.getString(encrypt(key), null); return v!=null ? Boolean.parseBoolean(decrypt(v)) : defValue; } @Override public float getFloat(String key, float defValue) { final String v = delegate.getString(encrypt(key), null); return v!=null ? Float.parseFloat(decrypt(v)) : defValue; } @Override public int getInt(String key, int defValue) { final String v = delegate.getString(encrypt(key), null); return v!=null ? Integer.parseInt(decrypt(v)) : defValue; } @Override public long getLong(String key, long defValue) { final String v = delegate.getString(encrypt(key), null); return v!=null ? Long.parseLong(decrypt(v)) : defValue; } @Override public String getString(String key, String defValue) { final String v = delegate.getString(encrypt(key), null); return v != null ? decrypt(v) : defValue; } @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 ) { try { final byte[] bytes = value!=null ? value.getBytes(UTF8) : new byte[0]; SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES"); SecretKey key = keyFactory.generateSecret(new PBEKeySpec(SEKRIT)); Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES"); pbeCipher.init(Cipher.ENCRYPT_MODE, key, new PBEParameterSpec(Settings.Secure.getString(context.getContentResolver(),Settings.Secure.ANDROID_ID).getBytes(UTF8), 20)); return new String(Base64.encode(pbeCipher.doFinal(bytes), Base64.NO_WRAP),UTF8); } catch( Exception e ) { throw new RuntimeException(e); } } protected String decrypt(String value){ try { final byte[] bytes = value!=null ? Base64.decode(value,Base64.DEFAULT) : new byte[0]; SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES"); SecretKey key = keyFactory.generateSecret(new PBEKeySpec(SEKRIT)); Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES"); pbeCipher.init(Cipher.DECRYPT_MODE, key, new PBEParameterSpec(Settings.Secure.getString(context.getContentResolver(),Settings.Secure.ANDROID_ID).getBytes(UTF8), 20)); return new String(pbeCipher.doFinal(bytes),UTF8); } catch( Exception e) { throw new RuntimeException(e); } } @Override public Set getStringSet(String arg0, Set arg1) { return null; } }
No entiendo lo de guardar la contraseña en recursos, ya que con APKTool, también se tiene acceso a esos ficheros, y al final tienes una contraseña en claro almacenada que es la utilizada para cifrar el fichero no?
ResponderEliminarTienes toda la razón, pero el objetivo no es hacer una aplicación totalmente segura, sino obstaculizar a los "newbies" que apenas han leído un tutorial de APKTool sin perjurdicarte a ti mismo como desarrollador. Me parece limpio puesto que evitas números y String mágicos en código (la clave) y ciertamente es más oscuro puesto que al String haces referencia por un int del tipo R.string.XXX y le dejas el trabajo de ponerse a buscar entre los recursos.
EliminarDependiendo de lo crítico de tu proyecto habría que plantearse subir la clave a algún servidor seguro con/sin certificados SSL, hashes, usar identificadores únicos del dispositivo...
En la versión actual le paso directamente la clave a esta clase como parámetro del constructor y así es muy fácil de reutilizar en cualquier proyecto. Pero la clave que le paso la sigo cargando desde archivos de recursos.