Protege tu APK: Datos del usuario 1

Lo ideal es no guardar datos de usuario de forma local, pero si no hay más remedio, es mejor de forma segura. ¿Cuál es la forma más sencilla para guardar este tipo de datos en Android?: "SharedPreferences", es un mecanismo extremadamente sencillo y fácil de utilizar. Lamentablemente guarda los datos en texto plano (sin cifrar), así que el que viene de fábrica no nos vale.



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, Set arg1) {
    // 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;
 }

}

2 comentarios:

  1. 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?

    ResponderEliminar
    Respuestas
    1. Tienes 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.

      Dependiendo 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.

      Eliminar